From 300f0fbeea460c2140a83057dad1f5e3991d6282 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Thu, 22 Oct 2026 23:44:08 +0530
Subject: [PATCH 01/93] docs(engine): track migration phases and baseline
evidence
---
docs/rust-engine/progress.md | 54 ++++++++++++++++++++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 docs/rust-engine/progress.md
diff --git a/docs/rust-engine/progress.md b/docs/rust-engine/progress.md
new file mode 100644
index 000000000..2421ed6e2
--- /dev/null
+++ b/docs/rust-engine/progress.md
@@ -0,0 +1,54 @@
+# Rust engine migration progress
+
+Implementation owner: sole agent; no delegation.
+Starting commit: `7b1ae05f204be53b5d9f2714f21c6eea08b25935`.
+Branch: `t3/rust-engine-migration`. Initial working tree clean.
+
+## Delivery
+
+Three PRs, stacked: Stage 1 engine → Stage 2 website → Stage 3 terminal.
+Small commits at coherent behavior/build/documentation boundaries. Do not create
+empty commits to inflate count. Do not publish production or releases implicitly.
+
+## Current position
+
+Stage 1, Phase 1.1 in progress. Read root AGENTS.md and the complete migration
+plan before edits. Installed workspace dependencies with frozen lockfile.
+No acceptance gate passed yet. Existing website engine remains authoritative.
+
+| Phase | Status | Evidence / remaining gate |
+| --- | --- | --- |
+| 1.1 Inventory / baselines | Active | Core baseline: 28 files, 349 tests pass. Browser/performance baseline pending. |
+| 1.2 Contracts / fixtures | Pending | Requires inventory and recorded baseline. |
+| 1.3 Native / browser proof | Pending | Requires frozen contracts. |
+| 1.4 Input / lifecycle / timing | Pending | Requires production browser proof. |
+| 1.5 Generation / configuration | Pending | Requires contract and proof. |
+| 1.6 Events / results / replay | Pending | Requires input and generation. |
+| 1.7 Binding / engine gate | Pending | Full three-path parity and budgets. |
+| 2.1–2.6 Website | Pending | Requires Stage 1 gate. |
+| 3.1–3.5 Terminal | Pending | Requires Stage 2 gate. |
+
+## Verification log
+
+- `vp install --frozen-lockfile`: pass; 1,102 packages, lockfile unchanged.
+- `vp test run --project=@oxytype/typing-core`: pass; 349 tests, 28 files.
+- Environment: Linux, Node 24.21.0; installed Rust 1.99.1; GitHub CLI authenticated.
+ Toolchain/binding pins not yet selected. Browser/OS support unverified.
+
+## Decisions
+
+- User's three-stage PR structure overrides the plan's suggested per-phase PRs.
+- Browser rendering constraints (visible previous word, overflow) must be explicit
+ host observations; Rust decides their rule effects. They cannot be discarded
+ merely because headless TestSession does not model them.
+- Preserve frozen snapshots. Record discrepancies; never regenerate to pass.
+
+## Blockers
+
+None established. Missing verification is pending work, not a passed gate.
+
+## Resume
+
+Read this file, inventory, contract, decisions, then `vp exec git status --short`.
+Continue the first incomplete gate. Record commands and actual outcomes here;
+do not rerun completed checks without a new reason.
From afae54ccf68741b4b0c8c2d253add3955666221d Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Thu, 22 Oct 2026 23:47:40 +0530
Subject: [PATCH 02/93] test(engine): freeze consumer inventory and browser
performance baseline
---
docs/rust-engine/baseline-input.json | 494 +++++++++++++++++++
docs/rust-engine/baseline-readiness.json | 17 +
docs/rust-engine/baseline.md | 48 ++
docs/rust-engine/features.md | 88 ++++
docs/rust-engine/inventory.md | 277 +++++++++++
docs/rust-engine/progress.md | 8 +-
docs/rust-engine/reference-manifest.json | 94 ++++
frontend/e2e/engine-performance.migration.ts | 87 ++++
8 files changed, 1109 insertions(+), 4 deletions(-)
create mode 100644 docs/rust-engine/baseline-input.json
create mode 100644 docs/rust-engine/baseline-readiness.json
create mode 100644 docs/rust-engine/baseline.md
create mode 100644 docs/rust-engine/features.md
create mode 100644 docs/rust-engine/inventory.md
create mode 100644 docs/rust-engine/reference-manifest.json
create mode 100644 frontend/e2e/engine-performance.migration.ts
diff --git a/docs/rust-engine/baseline-input.json b/docs/rust-engine/baseline-input.json
new file mode 100644
index 000000000..d8ead56e8
--- /dev/null
+++ b/docs/rust-engine/baseline-input.json
@@ -0,0 +1,494 @@
+{
+ "browser": "153.0.8010.12",
+ "fixture": "built Start Worker; desktop Chromium; zen; 20 input/restart samples; double-rAF paint; 2000 keyboard characters; forced GC",
+ "inputToPaint": [
+ 138.20000000298023, 80.20000000298023, 84, 88.1000000089407,
+ 68.1000000089407, 87.90000000596046, 80.5, 77.8999999910593,
+ 60.599999994039536, 73.59999999403954, 107.5, 78.1000000089407,
+ 59.1000000089407, 92, 76.90000000596046, 72.3999999910593, 57.5,
+ 87.69999998807907, 76.3999999910593, 89.20000000298023
+ ],
+ "restart": [
+ 388.47247100000004, 423.63136399999985, 433.59411, 426.2511919999997,
+ 449.42194900000004, 438.79164199999923, 409.1686869999994,
+ 415.29368799999975, 388.8231259999993, 448.3593600000004, 452.6419649999998,
+ 408.8987789999992, 401.96822299999985, 441.1246890000002, 436.5003859999997,
+ 401.4671980000003, 451.58758399999897, 385.44698999999855,
+ 423.49853500000063, 394.7576749999989
+ ],
+ "initialHeap": 8628096,
+ "activeHeap": 13696236,
+ "resetHeap": 13135484,
+ "assets": [
+ {
+ "name": "/assets/index-Bih2PTRA.js",
+ "bytes": 120018,
+ "transfer": 48207
+ },
+ {
+ "name": "/assets/web-DoCSsFQb.js",
+ "bytes": 32378,
+ "transfer": 12617
+ },
+ {
+ "name": "/assets/link-BBh-inDq.js",
+ "bytes": 62968,
+ "transfer": 22646
+ },
+ {
+ "name": "/assets/redirect-D6mi4iKQ.js",
+ "bytes": 848,
+ "transfer": 784
+ },
+ {
+ "name": "/assets/matchContext-CJdPZbyb.js",
+ "bytes": 106,
+ "transfer": 410
+ },
+ {
+ "name": "/assets/not-found-i5RsCZif.js",
+ "bytes": 118,
+ "transfer": 411
+ },
+ {
+ "name": "/assets/ClientOnly-BFOvw4I-.js",
+ "bytes": 315,
+ "transfer": 528
+ },
+ {
+ "name": "/assets/application-BO7jqupq.js",
+ "bytes": 1199,
+ "transfer": 898
+ },
+ {
+ "name": "/assets/useRouter-Ca7pFXE8.js",
+ "bytes": 103,
+ "transfer": 414
+ },
+ {
+ "name": "/assets/createServerFn-DlBie0Bv.js",
+ "bytes": 36231,
+ "transfer": 11909
+ },
+ {
+ "name": "/assets/store-CAx0GJVG.js",
+ "bytes": 7034,
+ "transfer": 2818
+ },
+ {
+ "name": "/assets/build-BK2Ekhsu.js",
+ "bytes": 15058,
+ "transfer": 5010
+ },
+ {
+ "name": "/assets/queryObserver-D957XF2z.js",
+ "bytes": 20747,
+ "transfer": 7040
+ },
+ {
+ "name": "/assets/preload-helper-BZ1Pz5am.js",
+ "bytes": 1218,
+ "transfer": 988
+ },
+ {
+ "name": "/assets/schemas-BCsEXk5z.js",
+ "bytes": 95477,
+ "transfer": 26790
+ },
+ {
+ "name": "/assets/dist-Dz3b91zX.js",
+ "bytes": 2701,
+ "transfer": 1352
+ },
+ {
+ "name": "/assets/cn-DLyFV4qv.js",
+ "bytes": 27805,
+ "transfer": 9057
+ },
+ {
+ "name": "/assets/skeleton-CWhZcH5J.js",
+ "bytes": 2536,
+ "transfer": 1185
+ },
+ {
+ "name": "/assets/skeleton-CtcKlTjt.js",
+ "bytes": 417,
+ "transfer": 608
+ },
+ {
+ "name": "/assets/skeleton-text-CMtoHw-F.js",
+ "bytes": 390,
+ "transfer": 591
+ },
+ {
+ "name": "/assets/shared-DsmhGVsE.js",
+ "bytes": 8590,
+ "transfer": 3686
+ },
+ {
+ "name": "/assets/skeleton-DZbZqgc0.js",
+ "bytes": 1235,
+ "transfer": 910
+ },
+ {
+ "name": "/assets/zod-messages-DFcmgwyp.js",
+ "bytes": 2774,
+ "transfer": 1398
+ },
+ {
+ "name": "/assets/_app-BOKTVhON.js",
+ "bytes": 158,
+ "transfer": 451
+ },
+ {
+ "name": "/assets/_app.index-DywpqF7e.js",
+ "bytes": 2637,
+ "transfer": 1607
+ },
+ {
+ "name": "/assets/browser-runtime.client-DqEaJ8Tp.js",
+ "bytes": 13557,
+ "transfer": 6118
+ },
+ {
+ "name": "/assets/personal-bests-DoKtXru2.js",
+ "bytes": 83199,
+ "transfer": 26749
+ },
+ {
+ "name": "/assets/default-config-Ch20GKSO.js",
+ "bytes": 2053,
+ "transfer": 1372
+ },
+ {
+ "name": "/assets/test-activity-calendar-lFGxz15v.js",
+ "bytes": 5837,
+ "transfer": 2611
+ },
+ {
+ "name": "/assets/users-DZf1VidT.js",
+ "bytes": 19012,
+ "transfer": 9304
+ },
+ {
+ "name": "/assets/_virtual_env-config-D6szLuD5.js",
+ "bytes": 196,
+ "transfer": 482
+ },
+ {
+ "name": "/assets/useRef-9UY2IK1A.js",
+ "bytes": 98,
+ "transfer": 415
+ },
+ {
+ "name": "/assets/app-BQPR1dmp.js",
+ "bytes": 53139,
+ "transfer": 16377
+ },
+ {
+ "name": "/assets/createEvent-DUwnlqx6.js",
+ "bytes": 313,
+ "transfer": 528
+ },
+ {
+ "name": "/assets/effects-7SE5KZhe.js",
+ "bytes": 38433,
+ "transfer": 15617
+ },
+ {
+ "name": "/assets/useLocalStorage-ICcLxfs0.js",
+ "bytes": 3971,
+ "transfer": 2259
+ },
+ {
+ "name": "/assets/parse-C_h2zvC4.js",
+ "bytes": 5672,
+ "transfer": 2774
+ },
+ {
+ "name": "/assets/test-C11xF37d.js",
+ "bytes": 13652,
+ "transfer": 5833
+ },
+ {
+ "name": "/assets/rolldown-runtime-Dd_uD5pT.js",
+ "bytes": 1106,
+ "transfer": 927
+ },
+ {
+ "name": "/assets/env-hNDnyPZd.js",
+ "bytes": 100,
+ "transfer": 412
+ },
+ {
+ "name": "/assets/auth-client-Do4XO1Ss.js",
+ "bytes": 1099,
+ "transfer": 907
+ },
+ {
+ "name": "/assets/connection-BMlZb37h.js",
+ "bytes": 903,
+ "transfer": 837
+ },
+ {
+ "name": "/assets/popup.client-Cozt_Hi9.js",
+ "bytes": 31904,
+ "transfer": 12210
+ },
+ {
+ "name": "/assets/auth-EgLHI1c9.js",
+ "bytes": 7905,
+ "transfer": 3646
+ },
+ {
+ "name": "/assets/date-and-time-Bxt-WH9L.js",
+ "bytes": 964,
+ "transfer": 795
+ },
+ {
+ "name": "/assets/settled.client-BoF65wsR.js",
+ "bytes": 530,
+ "transfer": 644
+ },
+ {
+ "name": "/assets/query-cache.client-Bht1In51.js",
+ "bytes": 3064,
+ "transfer": 1726
+ },
+ {
+ "name": "/assets/build-hTByFGMt.js",
+ "bytes": 3318,
+ "transfer": 1634
+ },
+ {
+ "name": "/assets/settled.client-H6Sptnm2.js",
+ "bytes": 349,
+ "transfer": 558
+ },
+ {
+ "name": "/assets/update-safety-C66gpHLz.js",
+ "bytes": 140,
+ "transfer": 443
+ },
+ {
+ "name": "/assets/connection-DQSF9opu.js",
+ "bytes": 6679,
+ "transfer": 3042
+ },
+ {
+ "name": "/assets/cookies-DAiHfj0P.js",
+ "bytes": 691,
+ "transfer": 724
+ },
+ {
+ "name": "/assets/glarses-mode-SixLinXc.js",
+ "bytes": 285,
+ "transfer": 524
+ },
+ {
+ "name": "/assets/fonts.client-CxGm7qeF.js",
+ "bytes": 840,
+ "transfer": 815
+ },
+ {
+ "name": "/assets/chrome.client-BT71_e9g.js",
+ "bytes": 41572,
+ "transfer": 14441
+ },
+ {
+ "name": "/assets/mode-BgIo3a2E.js",
+ "bytes": 326,
+ "transfer": 514
+ },
+ {
+ "name": "/assets/list-Fui5YVEc.js",
+ "bytes": 72086,
+ "transfer": 25222
+ },
+ {
+ "name": "/assets/Kbd-DPDwH4vf.js",
+ "bytes": 1953,
+ "transfer": 1436
+ },
+ {
+ "name": "/assets/overlay-visibility-CHOMN59j.js",
+ "bytes": 181,
+ "transfer": 461
+ },
+ {
+ "name": "/assets/UpdateNameModal-DCeulr76.js",
+ "bytes": 6845,
+ "transfer": 3187
+ },
+ {
+ "name": "/assets/breakpoints-Cc6HYHps.js",
+ "bytes": 111,
+ "transfer": 409
+ },
+ {
+ "name": "/assets/simple-modal-QQBzpojW.js",
+ "bytes": 255,
+ "transfer": 501
+ },
+ {
+ "name": "/assets/utils-OCzDnQsQ.js",
+ "bytes": 638,
+ "transfer": 665
+ },
+ {
+ "name": "/assets/modifier-keys-CBdW-khj.js",
+ "bytes": 165,
+ "transfer": 437
+ },
+ {
+ "name": "/assets/navigate-H3LNuvCa.js",
+ "bytes": 268,
+ "transfer": 469
+ },
+ {
+ "name": "/assets/anime-CmdYVwG7.js",
+ "bytes": 5568,
+ "transfer": 2629
+ },
+ {
+ "name": "/assets/levels-DlYwc516.js",
+ "bytes": 414,
+ "transfer": 593
+ },
+ {
+ "name": "/assets/page.client-DkiCL-AL.js",
+ "bytes": 77067,
+ "transfer": 23940
+ },
+ {
+ "name": "/assets/modifiers-Cavid0O1.js",
+ "bytes": 1872,
+ "transfer": 1176
+ },
+ {
+ "name": "/assets/practise-words-DX1fN1hH.js",
+ "bytes": 2060,
+ "transfer": 1337
+ },
+ {
+ "name": "/assets/quote-rate-D0u-bc68.js",
+ "bytes": 42597,
+ "transfer": 14482
+ },
+ {
+ "name": "/assets/test-logic-DBD-TUd8.js",
+ "bytes": 32336,
+ "transfer": 11982
+ },
+ {
+ "name": "/assets/quotes-controller-NMBMwU9J.js",
+ "bytes": 2830,
+ "transfer": 1500
+ },
+ {
+ "name": "/assets/format-BdXx0Xan.js",
+ "bytes": 187,
+ "transfer": 460
+ },
+ {
+ "name": "/assets/open-8_Twa7Cq.js",
+ "bytes": 253,
+ "transfer": 492
+ },
+ {
+ "name": "/assets/challenge-controller-BsG6c5c5.js",
+ "bytes": 3970,
+ "transfer": 1879
+ },
+ {
+ "name": "/assets/lz-ts-BjRkF-Z2.js",
+ "bytes": 3789,
+ "transfer": 1602
+ },
+ {
+ "name": "/assets/Result-KOjBV5UZ.js",
+ "bytes": 36010,
+ "transfer": 12527
+ },
+ {
+ "name": "/assets/tags-x7qZ75SS.js",
+ "bytes": 6056,
+ "transfer": 2476
+ },
+ {
+ "name": "/assets/reader-CD0iXdWl.js",
+ "bytes": 336480,
+ "transfer": 92657
+ },
+ {
+ "name": "/assets/UserFlags-D0zcp66J.js",
+ "bytes": 994,
+ "transfer": 788
+ },
+ {
+ "name": "/assets/typing-speed-units-C-jdytt1.js",
+ "bytes": 37,
+ "transfer": 357
+ },
+ {
+ "name": "/assets/edit-result-tags-aFuXCmB7.js",
+ "bytes": 294,
+ "transfer": 531
+ },
+ {
+ "name": "/assets/result-Bi9gnFH6.js",
+ "bytes": 19238,
+ "transfer": 7675
+ },
+ {
+ "name": "/assets/today-tracker-Bgf7zsww.js",
+ "bytes": 2349,
+ "transfer": 1484
+ },
+ {
+ "name": "/assets/test-screenshot-BLnrMuiq.js",
+ "bytes": 4898,
+ "transfer": 2604
+ },
+ {
+ "name": "/assets/quote-report-CqoAU8jd.js",
+ "bytes": 163,
+ "transfer": 453
+ },
+ {
+ "name": "/assets/completion.client-py8Tupf4.js",
+ "bytes": 6348,
+ "transfer": 3293
+ },
+ {
+ "name": "/assets/result-saving-BzV5NqD0.js",
+ "bytes": 4902,
+ "transfer": 2525
+ },
+ {
+ "name": "/assets/ResultChart-CoVlHRBs.js",
+ "bytes": 6050,
+ "transfer": 2766
+ },
+ {
+ "name": "/assets/ChartJs-CYa-nlCR.js",
+ "bytes": 74721,
+ "transfer": 22585
+ },
+ {
+ "name": "/assets/endOfWeek-yDY4_qaX.js",
+ "bytes": 1860,
+ "transfer": 1085
+ },
+ {
+ "name": "/assets/compareAsc-BJhcP0hn.js",
+ "bytes": 105,
+ "transfer": 422
+ },
+ {
+ "name": "/assets/chart-Bx03QiIs.js",
+ "bytes": 177979,
+ "transfer": 61565
+ }
+ ]
+}
diff --git a/docs/rust-engine/baseline-readiness.json b/docs/rust-engine/baseline-readiness.json
new file mode 100644
index 000000000..2a5df4452
--- /dev/null
+++ b/docs/rust-engine/baseline-readiness.json
@@ -0,0 +1,17 @@
+{
+ "fixture": "production assets, guest API fixtures, desktop Chromium, HTTP cache cold/warm, SW bypass",
+ "cold": [
+ 745.9000000059605, 752.2999999970198, 755.7000000029802, 714.2999999970198,
+ 742.0999999940395, 670.1000000089407, 1259.7999999970198,
+ 1400.6000000089407, 713.7000000029802, 1347.6000000089407
+ ],
+ "warm": [
+ 728.8999999910593, 625.7000000029802, 609.0999999940395, 638.7000000029802,
+ 644.7000000029802, 711.0999999940395, 744.4000000059605, 634.3999999910593,
+ 662.8000000119209, 722.9000000059605
+ ],
+ "settings": [
+ 522, 494.6000000089407, 498.90000000596046, 485.8999999910593, 716.5, 536.5,
+ 690.1000000089407, 508, 519.2999999970198, 558.1000000089407
+ ]
+}
diff --git a/docs/rust-engine/baseline.md b/docs/rust-engine/baseline.md
new file mode 100644
index 000000000..cf4ddce55
--- /dev/null
+++ b/docs/rust-engine/baseline.md
@@ -0,0 +1,48 @@
+# Website baseline
+
+Source engine: `7b1ae05f204be53b5d9f2714f21c6eea08b25935`. No rule changes.
+
+Host: Linux aarch64, 7.1.13-402.asahi.fc44.aarch64+16k; Node 24.21.0.
+Browser: bundled Playwright Chromium 153.0.8010.12, Desktop Chrome profile.
+No CPU/network throttle. Shared development machine; no exclusive CPU reservation.
+Cloudflare Vite + TanStack Start production bundle, local Miniflare/workerd, seeded D1.
+Browser HTTP cache cold/warm as described in readiness fixture; service worker bypassed.
+
+| Measurement (ms) | Samples | Median | p95 |
+| --- | --- | --- | --- |
+| cold | 10 | 745.90 | 1400.60 |
+| warm | 10 | 644.70 | 744.40 |
+| settings | 10 | 519.30 | 716.50 |
+| inputToPaint | 20 | 78.10 | 107.50 |
+| restart | 20 | 423.50 | 451.59 |
+
+Readiness includes navigation → first accepted input/render, so measures usable test,
+not only bundle initialization. Input-to-paint is a conservative double-rAF proxy
+including Playwright assertion latency. Restart includes click + complete fade.
+Raw samples are adjacent JSON, not replaced by summary numbers.
+
+Forced-GC JS heap: initial 8,628,096 B; active after 2,000 keyboard chars 13,696,236 B; after reset 13,135,484 B.
+Recorded JS/Wasm decoded resource bytes: 1,717,897 B (resource list in JSON; repeated URLs count repeated loads).
+This bounds one 1,000-word zen run; longer soak verification still required at 1.7/2.5.
+
+## Reproduce
+
+```sh
+vp install --frozen-lockfile
+vp run --filter @oxytype/frontend build:start
+MIGRATION_BASELINE_OUTPUT="$PWD/docs/rust-engine/candidate-readiness.json" ENGINE_PERFORMANCE_OUTPUT="$PWD/docs/rust-engine/candidate-input.json" vp run --filter @oxytype/frontend benchmark-migration readiness.migration.ts engine-performance.migration.ts
+```
+
+The fixture server expects `dist/start`, not the `dist/start-test` runtime-entry build.
+Initial attempt used the latter and failed ENOENT before browser work; corrected by
+building `build:start`. Both build paths passed; production path browser tests passed.
+
+## Existing regression evidence
+
+- Core: 28 files / 349 tests passed.
+- Frontend typing runtime: 27 files / 475 tests passed.
+- Frontend input: 9 files / 204 tests passed.
+- Browser baseline: 2 tests passed (53.4 s).
+- Baseline benchmark type-aware lint passed.
+- Production build emits pre-existing CSS sourcemap warning; build exits 0.
+
diff --git a/docs/rust-engine/features.md b/docs/rust-engine/features.md
new file mode 100644
index 000000000..dc91d4fbd
--- /dev/null
+++ b/docs/rust-engine/features.md
@@ -0,0 +1,88 @@
+# Feature parity checklist
+
+Every entry must pass before Stage 1 / web cutover. Pending means unported.
+
+| Feature | Rust | Native / Wasm / web evidence |
+| --- | --- | --- |
+| words: finite / zero infinite | Pending | Existing fixtures + targeted gaps required |
+| time: presets / custom / zero infinite | Pending | Existing fixtures + targeted gaps required |
+| quote: lengths / IDs / repeat / British text | Pending | Existing fixtures + targeted gaps required |
+| custom: random / repeat / shuffle; pipe sections; word / time / section limits | Pending | Existing fixtures + targeted gaps required |
+| zen: free typing / bailout / trailing idle | Pending | Existing fixtures + targeted gaps required |
+| language: lazy accents / additional accents / noLazyMode | Pending | Existing fixtures + targeted gaps required |
+| punctuation / original punctuation / numbers / numeral scripts | Pending | Existing fixtures + targeted gaps required |
+| wordset: random / Zipf / shuffled / previous-two exclusion | Pending | Existing fixtures + targeted gaps required |
+| funbox sections / polyglot language properties / asynchronous content | Pending | Existing fixtures + targeted gaps required |
+| normal / expert / master difficulty | Pending | Existing fixtures + targeted gaps required |
+| strictSpace / stopOnError letter and word | Pending | Existing fixtures + targeted gaps required |
+| deleteOnError letter / word / hard; stopped-input interaction | Pending | Existing fixtures + targeted gaps required |
+| confidence off / on / max; freedomMode; visible-word bound | Pending | Existing fixtures + targeted gaps required |
+| UTF-16 split commits / partial surrogates / grapheme-independent scores | Pending | Existing fixtures + targeted gaps required |
+| IME multi-character / composition quick finish / equivalent spaces | Pending | Existing fixtures + targeted gaps required |
+| typography equivalents / ellipsis / Dutch ligature / opposite Shift | Pending | Existing fixtures + targeted gaps required |
+| code automatic tabs / unindent / newline / Firefox deletion | Pending | Existing fixtures + targeted gaps required |
+| timer drift / catchup / background stalls / simultaneous timestamps | Pending | Existing fixtures + targeted gaps required |
+| reset / restart / repeat / abandon / completion once / stale commands | Pending | Existing fixtures + targeted gaps required |
+| minimum burst / speed / accuracy; flex burst | Pending | Existing fixtures + targeted gaps required |
+| pace correction / remembered speed / weak-spot learning | Pending | Existing fixtures + targeted gaps required |
+| accuracy / WPM / raw / chars / consistency / burst | Pending | Existing fixtures + targeted gaps required |
+| physical timing / synthetic observations / estimated release markers | Pending | Existing fixtures + targeted gaps required |
+| histories / charts / idle / incomplete duration / validity | Pending | Existing fixtures + targeted gaps required |
+| historical replay / seek / regression / clearedNextWord | Pending | Existing fixtures + targeted gaps required |
+| CompletedEvent fields / schemas / submission hash / backend rejection | Pending | Existing fixtures + targeted gaps required |
+| settings imports / presets / URL overrides / live rules | Pending | Existing fixtures + targeted gaps required |
+| custom text progress / practice missed and slow words | Pending | Existing fixtures + targeted gaps required |
+| SSR safety / Cloudflare asset MIME+CSP / stale tabs / rollback | Pending | Existing fixtures + targeted gaps required |
+
+## Every funbox
+
+| Name | Rule / host ownership | Evidence |
+| --- | --- | --- |
+| `58008` | Rust rules; retain any visual/input hooks in host | Pending |
+| `mirror` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `upside_down` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `nausea` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `round_round_baby` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `simon_says` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `tts` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `choo_choo` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `arrows` | Rust rules; retain any visual/input hooks in host | Pending |
+| `rAnDoMcAsE` | Rust rules; retain any visual/input hooks in host | Pending |
+| `sPoNgEcAsE` | Rust rules; retain any visual/input hooks in host | Pending |
+| `capitals` | Rust rules; retain any visual/input hooks in host | Pending |
+| `layout_mirror` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `layoutfluid` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `earthquake` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `space_balls` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `gibberish` | Rust rules; retain any visual/input hooks in host | Pending |
+| `ascii` | Rust rules; retain any visual/input hooks in host | Pending |
+| `specials` | Rust rules; retain any visual/input hooks in host | Pending |
+| `plus_zero` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `plus_one` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `plus_two` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `plus_three` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `read_ahead_easy` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `read_ahead` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `read_ahead_hard` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `memory` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `nospace` | Rust rules; retain any visual/input hooks in host | Pending |
+| `poetry` | Rust rules; retain any visual/input hooks in host | Pending |
+| `wikipedia` | Rust rules; retain any visual/input hooks in host | Pending |
+| `weakspot` | Rust rules; retain any visual/input hooks in host | Pending |
+| `pseudolang` | Rust rules; retain any visual/input hooks in host | Pending |
+| `IPv4` | Rust rules; retain any visual/input hooks in host | Pending |
+| `IPv6` | Rust rules; retain any visual/input hooks in host | Pending |
+| `binary` | Rust rules; retain any visual/input hooks in host | Pending |
+| `hexadecimal` | Rust rules; retain any visual/input hooks in host | Pending |
+| `zipf` | Rust rules; retain any visual/input hooks in host | Pending |
+| `morse` | Rust rules; retain any visual/input hooks in host | Pending |
+| `crt` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `backwards` | Rust rules; retain any visual/input hooks in host | Pending |
+| `ddoouubblleedd` | Rust rules; retain any visual/input hooks in host | Pending |
+| `instant_messaging` | Rust rules; retain any visual/input hooks in host | Pending |
+| `underscore_spaces` | Rust rules; retain any visual/input hooks in host | Pending |
+| `ALL_CAPS` | Rust rules; retain any visual/input hooks in host | Pending |
+| `polyglot` | Rust rules; retain any visual/input hooks in host | Pending |
+| `asl` | Host visual/audio/layout; Rust retains metadata effects on eligibility/config | Pending |
+| `rot13` | Rust rules; retain any visual/input hooks in host | Pending |
+| `no_quit` | Rust rules; retain any visual/input hooks in host | Pending |
diff --git a/docs/rust-engine/inventory.md b/docs/rust-engine/inventory.md
new file mode 100644
index 000000000..2ce06c801
--- /dev/null
+++ b/docs/rust-engine/inventory.md
@@ -0,0 +1,277 @@
+# Migration inventory
+
+Frozen at `7b1ae05f204be53b5d9f2714f21c6eea08b25935`. Sole implementation owner.
+
+## Source ownership and destination
+
+| Current module | Classification | Destination |
+| --- | --- | --- |
+| `active-funboxes.ts` | Engine | crates/typing-engine |
+| `arrays.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `british-english-replacements.ts` | Content asset | shared replacement data |
+| `british-english.ts` | Engine | crates/typing-engine |
+| `command-matching.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `completed-event.ts` | Engine | crates/typing-engine |
+| `config/default-config.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `config/funbox-validation.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `config/metadata.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `config/migrate.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `config/setter.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `custom-text.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `ddr.ts` | Engine | crates/typing-engine |
+| `english-punctuation.ts` | Engine | crates/typing-engine |
+| `errors.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `events/data.ts` | Engine | crates/typing-engine |
+| `events/helpers.ts` | Engine | crates/typing-engine |
+| `events/live-cache.ts` | Engine | crates/typing-engine |
+| `events/stats/bursts.ts` | Engine | crates/typing-engine |
+| `events/stats/characters.ts` | Engine | crates/typing-engine |
+| `events/stats/intervals.ts` | Engine | crates/typing-engine |
+| `events/stats/key-timing.ts` | Engine | crates/typing-engine |
+| `events/stats/speed-history.ts` | Engine | crates/typing-engine |
+| `events/stats/timing.ts` | Engine | crates/typing-engine |
+| `events/stats/word-history.ts` | Engine | crates/typing-engine |
+| `events/stats.ts` | Engine | crates/typing-engine |
+| `events/types.ts` | Shared contract | generated bindings + checked web DTOs |
+| `format.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `funbox-word-functions.ts` | Engine | crates/typing-engine |
+| `generate.ts` | Engine | crates/typing-engine |
+| `index.ts` | Re-export | retire |
+| `input/config.ts` | Shared contract | generated bindings + checked web DTOs |
+| `input/engine.ts` | Engine | crates/typing-engine |
+| `input/fail-or-finish.ts` | Engine | crates/typing-engine |
+| `input/input-type.ts` | Shared contract | generated bindings + checked web DTOs |
+| `input/util.ts` | Engine | crates/typing-engine |
+| `input/validation.ts` | Engine | crates/typing-engine |
+| `ip-addresses.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `keys.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `languages.ts` | Content host adapter | TypeScript content adapters; Rust selection/transforms |
+| `layout-emulator.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `lazy-mode.ts` | Engine | crates/typing-engine |
+| `live-stats.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `mode.ts` | Engine | crates/typing-engine |
+| `numerals.ts` | Engine | crates/typing-engine |
+| `pace-caret.ts` | Engine | crates/typing-engine |
+| `poetry.ts` | Content host adapter | TypeScript content adapters; Rust selection/transforms |
+| `practise-words.ts` | Engine | crates/typing-engine |
+| `quick-restart.ts` | Engine | crates/typing-engine |
+| `quote-source.ts` | Content host adapter | TypeScript content adapters; Rust selection/transforms |
+| `quotes.ts` | Engine | crates/typing-engine |
+| `replay.ts` | Engine | crates/typing-engine |
+| `restart.ts` | Engine | crates/typing-engine |
+| `result-fields.ts` | Engine | crates/typing-engine |
+| `result-hash.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `result-validity.ts` | Engine | crates/typing-engine |
+| `session.ts` | Engine | crates/typing-engine |
+| `stats-math.ts` | Engine | crates/typing-engine |
+| `strings.ts` | Engine | crates/typing-engine |
+| `themes.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `timer.ts` | Engine | crates/typing-engine |
+| `typing-speed-units.ts` | Presentation/settings/transport | scoped TypeScript packages, SSR safe |
+| `typing-time.ts` | Engine | crates/typing-engine |
+| `weak-spot.ts` | Engine | crates/typing-engine |
+| `wikipedia.ts` | Content host adapter | TypeScript content adapters; Rust selection/transforms |
+| `words-generator/funboxes.ts` | Engine | crates/typing-engine |
+| `words-generator/limits.ts` | Engine | crates/typing-engine |
+| `words-generator/next-word.ts` | Engine | crates/typing-engine |
+| `words-generator/punctuation.ts` | Engine | crates/typing-engine |
+| `words-generator/quotes.ts` | Engine | crates/typing-engine |
+| `words-generator/state.ts` | Engine | crates/typing-engine |
+| `words-generator/transforms.ts` | Engine | crates/typing-engine |
+| `words-generator/types.ts` | Shared contract | generated bindings + checked web DTOs |
+| `words-generator.ts` | Engine | crates/typing-engine |
+| `wordset.ts` | Engine | crates/typing-engine |
+
+Split `strings`: display utilities stay TS; input normalization/countChars move to Rust.
+Settings metadata/migration stays TS; rule policies/defaults generated from Rust.
+`live-stats` formats display; score derivation moves to Rust.
+`ip-addresses` includes funbox generation: move those rules to Rust.
+`layout-emulator` is host key translation, supplied as observations.
+
+## Consumer map
+
+Includes mocks, scripts, type-only imports, server imports, re-exports, and manifests.
+
+```text
+./frontend/src/ts/utils/result-hash.ts:1:export * from "@oxytype/typing-core/result-hash";
+./frontend/src/ts/utils/quick-restart.ts:1:export { canQuickRestart } from "@oxytype/typing-core/quick-restart";
+./frontend/src/ts/utils/misc.ts:72:export { getMode2 } from "@oxytype/typing-core/mode";
+./frontend/src/ts/utils/json-data.ts:9:} from "@oxytype/typing-core/languages";
+./frontend/src/ts/utils/ip-addresses.ts:1:export * from "@oxytype/typing-core/ip-addresses";
+./frontend/src/ts/utils/format.ts:1:export { Formatting } from "@oxytype/typing-core/format";
+./frontend/package.json:62: "@oxytype/typing-core": "workspace:*",
+./frontend/src/ts/utils/arrays.ts:5:} from "@oxytype/typing-core/arrays";
+./frontend/src/ts/utils/typing-speed-units.ts:1:export { get } from "@oxytype/typing-core/typing-speed-units";
+./frontend/src/ts/utils/typing-speed-units.ts:2:export type { TypingSpeedUnitSettings } from "@oxytype/typing-core/typing-speed-units";
+./frontend/src/ts/input/helpers/word-navigation.ts:1:import { getPreviousWordInput } from "@oxytype/typing-core/input/engine";
+./frontend/src/ts/input/helpers/word-navigation.ts:21:import { getWordBurst } from "@oxytype/typing-core/events/stats";
+./frontend/src/ts/input/helpers/validation.ts:1:import * as Core from "@oxytype/typing-core/input/validation";
+./frontend/src/ts/utils/strings.ts:2:import { removeLanguageSize } from "@oxytype/typing-core/strings";
+./frontend/src/ts/utils/strings.ts:9:} from "@oxytype/typing-core/strings";
+./frontend/src/ts/utils/strings.ts:103:export { cleanTypographySymbols } from "@oxytype/typing-core/strings";
+./frontend/src/ts/utils/strings.ts:185:export { areCharactersVisuallyEqual } from "@oxytype/typing-core/strings";
+./frontend/src/ts/utils/strings.ts:205:export { isSpace } from "@oxytype/typing-core/strings";
+./frontend/src/ts/utils/strings.ts:215:export { countChars } from "@oxytype/typing-core/strings";
+./frontend/src/ts/input/helpers/util.ts:1:import * as Core from "@oxytype/typing-core/input/util";
+./frontend/src/ts/input/helpers/input-type.ts:1:export * from "@oxytype/typing-core/input/input-type";
+./frontend/src/ts/input/helpers/fail-or-finish.ts:1:import * as Core from "@oxytype/typing-core/input/fail-or-finish";
+./frontend/src/ts/input/handlers/insert-text.ts:1:import { evaluateInsert } from "@oxytype/typing-core/input/engine";
+./frontend/src/ts/input/handlers/before-insert-text.ts:1:import { shouldBlockInsertion } from "@oxytype/typing-core/input/engine";
+./frontend/src/ts/input/handlers/before-delete.ts:1:import { canDelete } from "@oxytype/typing-core/input/engine";
+./frontend/src/ts/states/test.ts:7:import { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/states/test.ts:16:import { QuoteWithTextSplit } from "@oxytype/typing-core/quotes";
+./frontend/src/ts/states/result.ts:10:import type { ReplayWord } from "@oxytype/typing-core/replay";
+./frontend/src/ts/states/live-stats.ts:3:import * as LiveStats from "@oxytype/typing-core/live-stats";
+./frontend/src/ts/states/event-log-viewer.ts:6:} from "@oxytype/typing-core/events/types";
+./frontend/src/ts/controllers/quotes-controller.ts:4:import { QuotesController as CoreQuotesController } from "@oxytype/typing-core/quote-source";
+./frontend/src/ts/controllers/quotes-controller.ts:5:import { Quote } from "@oxytype/typing-core/quotes";
+./frontend/src/ts/controllers/quotes-controller.ts:6:export type { Quote } from "@oxytype/typing-core/quotes";
+./frontend/src/ts/constants/themes.ts:1:export * from "@oxytype/typing-core/themes";
+./frontend/src/ts/constants/keys.ts:1:import { Keycode } from "@oxytype/typing-core/keys";
+./frontend/src/ts/constants/keys.ts:2:export type { Keycode } from "@oxytype/typing-core/keys";
+./frontend/src/ts/constants/default-config.ts:2:import { getDefaultConfig as getBaseDefaultConfig } from "@oxytype/typing-core/config/default-config";
+./frontend/src/ts/constants/default-config.ts:6:} from "@oxytype/typing-core/themes";
+./frontend/src/ts/config/utils.ts:5:} from "@oxytype/typing-core/config/migrate";
+./frontend/src/ts/config/metadata.tsx:8:} from "@oxytype/typing-core/config/metadata";
+./frontend/src/ts/config/lifecycle.ts:22:import { lastConfigsToApply } from "@oxytype/typing-core/config/setter";
+./frontend/src/ts/config/funbox-validation.ts:5:} from "@oxytype/typing-core/config/funbox-validation";
+./frontend/src/ts/components/modals/EventLogViewerModal.tsx:1:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/video-sync.ts:2:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/timeline-lanes.ts:5:} from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/PreviewContent.tsx:1:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/EventsPanel.tsx:4:} from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/scatter-points.ts:1:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/DistributionPanel.tsx:1:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/DistributionPanel.tsx:6:} from "@oxytype/typing-core/events/stats";
+./frontend/src/ts/components/modals/event-log-viewer/event-log.ts:5:} from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/event-log.ts:7:import { getInputFromDom } from "@oxytype/typing-core/events/helpers";
+./frontend/src/ts/components/modals/event-log-viewer/event-log.ts:8:import { EVENT_LOG_VERSION } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/createVideoSync.ts:9:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/createPlayback.ts:3:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/Timeline.tsx:1:import type { TestEventType } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/ScatterPanel.tsx:1:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/src/ts/components/modals/event-log-viewer/ScatterPanel.tsx:6:} from "@oxytype/typing-core/events/stats";
+./frontend/src/ts/commandline/matching.ts:1:export { matchCommands } from "@oxytype/typing-core/command-matching";
+./frontend/src/ts/commandline/lists/result-screen.ts:13:import { getInputHistory } from "@oxytype/typing-core/events/stats";
+./frontend/src/app/theme-vars.ts:1:import type { Theme } from "@oxytype/typing-core/themes";
+./frontend/src/app/theme-bootstrap.ts:1:import { defaultThemes } from "@oxytype/typing-core/config/default-config";
+./frontend/src/app/theme-bootstrap.ts:2:import { themes } from "@oxytype/typing-core/themes";
+./frontend/src/features/typing/runtime/words-generator.ts:1:import { createWordsGenerator } from "@oxytype/typing-core/words-generator";
+./frontend/src/features/typing/runtime/words-generator.ts:10:import * as LazyMode from "@oxytype/typing-core/lazy-mode";
+./frontend/src/features/typing/runtime/words-generator.ts:11:import * as EnglishPunctuation from "@oxytype/typing-core/english-punctuation";
+./frontend/src/features/typing/runtime/wikipedia.ts:1:import { getSection as loadSection } from "@oxytype/typing-core/wikipedia";
+./frontend/src/features/typing/runtime/wikipedia.ts:2:import { Section } from "@oxytype/typing-core/languages";
+./frontend/src/features/typing/runtime/weak-spot.ts:1:import { createWeakSpot } from "@oxytype/typing-core/weak-spot";
+./frontend/src/features/typing/runtime/layout-emulator.ts:2:import { getCharFromLayout } from "@oxytype/typing-core/layout-emulator";
+./frontend/src/features/typing/runtime/today-tracker.ts:4:} from "@oxytype/typing-core/typing-time";
+./frontend/src/features/typing/runtime/initialization.ts:42:import { WordGenError } from "@oxytype/typing-core/errors";
+./frontend/src/features/typing/runtime/initialization.ts:54:import { hasKoreanCharacters } from "@oxytype/typing-core/strings";
+./frontend/src/features/typing/runtime/timer-scheduler.ts:1:import type { TestTimer } from "@oxytype/typing-core/timer";
+./frontend/src/features/typing/runtime/custom-text.ts:8:import { defaultCustomTextSettings } from "@oxytype/typing-core/custom-text";
+./frontend/src/features/typing/runtime/completion.client.ts:63:} from "@oxytype/typing-core/events/stats";
+./frontend/src/features/typing/runtime/completion.client.ts:68:} from "@oxytype/typing-core/result-validity";
+./frontend/src/features/typing/runtime/completion.client.ts:72:import { countInvalidResultFields } from "@oxytype/typing-core/result-fields";
+./frontend/src/features/typing/runtime/british-english.ts:1:import { replace as replaceWord } from "@oxytype/typing-core/british-english";
+./frontend/src/features/typing/runtime/test-timer.ts:3:import type { SessionEvents } from "@oxytype/typing-core/session";
+./frontend/src/features/typing/runtime/test-timer.ts:4:import { getTimeLimit } from "@oxytype/typing-core/timer";
+./frontend/src/app/default-theme.ts:2:import { defaultThemes } from "@oxytype/typing-core/config/default-config";
+./frontend/src/features/typing/runtime/result/history.ts:12:} from "@oxytype/typing-core/events/stats";
+./frontend/src/features/typing/runtime/test-logic.ts:76:import { shouldRepeatQuote } from "@oxytype/typing-core/restart";
+./frontend/src/features/typing/runtime/result/chart-data.ts:15:} from "@oxytype/typing-core/events/stats";
+./frontend/src/features/typing/runtime/result/accuracy.ts:2:import { getAccuracy } from "@oxytype/typing-core/events/stats";
+./frontend/src/server/operation.server.ts:19:import { migrateConfig } from "@oxytype/typing-core/config/migrate";
+./frontend/src/features/typing/runtime/replay.ts:5:import { getWpmHistory } from "@oxytype/typing-core/events/stats";
+./frontend/src/features/typing/runtime/replay.ts:12:} from "@oxytype/typing-core/replay";
+./frontend/src/features/typing/runtime/funbox/funbox-functions.ts:2:import { FunboxWordFunctions } from "@oxytype/typing-core/active-funboxes";
+./frontend/src/features/typing/runtime/funbox/funbox-functions.ts:3:import { createFunboxWordFunctions } from "@oxytype/typing-core/funbox-word-functions";
+./frontend/src/features/typing/runtime/practise-words.ts:1:import { buildPracticeWords } from "@oxytype/typing-core/practise-words";
+./frontend/src/features/typing/runtime/events/live-cache.ts:3:import { createLiveCache } from "@oxytype/typing-core/events/live-cache";
+./frontend/src/features/typing/runtime/events/data.ts:4:import { createTestSession } from "@oxytype/typing-core/session";
+./frontend/src/features/typing/runtime/events/data.ts:5:import { EventLogContext } from "@oxytype/typing-core/events/types";
+./frontend/src/features/typing/runtime/poetry.ts:1:import { getPoem as loadPoem } from "@oxytype/typing-core/poetry";
+./frontend/src/features/typing/runtime/poetry.ts:2:import { Section } from "@oxytype/typing-core/languages";
+./frontend/src/features/typing/components/result/ResultWordsHistory.tsx:5:} from "@oxytype/typing-core/events/stats";
+./frontend/src/features/typing/components/result/ResultReplay.tsx:1:import type { ReplayLetter } from "@oxytype/typing-core/replay";
+./frontend/src/features/typing/components/result/ResultChart.tsx:8:import { getWordIndexesForSecond } from "@oxytype/typing-core/events/stats";
+./frontend/src/features/settings/sidebar-skeleton.tsx:1:import { getDefaultConfig } from "@oxytype/typing-core/config/default-config";
+./frontend/scripts/measure-input-snapshots.ts:2:import { createEventRecorder } from "@oxytype/typing-core/events/data";
+./frontend/scripts/measure-input-snapshots.ts:3:import { createLiveCache } from "@oxytype/typing-core/events/live-cache";
+./frontend/__tests__/input/helpers/fail-or-finish.spec.ts:8:import * as Misc from "@oxytype/typing-core/mode";
+./frontend/__tests__/input/helpers/fail-or-finish.spec.ts:13:vi.mock("@oxytype/typing-core/mode", async (importOriginal) => {
+./frontend/__tests__/input/helpers/fail-or-finish.spec.ts:15: await importOriginal();
+./frontend/__tests__/input/handlers/insert-text.spec.ts:143:} from "@oxytype/typing-core/events/helpers";
+./frontend/__tests__/input/handlers/insert-text.spec.ts:144:import type { InputEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/timer-scheduler.spec.ts:1:import { createTestTimer, type TimerTick } from "@oxytype/typing-core/timer";
+./frontend/__tests__/test/test-logic.spec.ts:6:import type { QuoteWithTextSplit } from "@oxytype/typing-core/quotes";
+./frontend/__tests__/test/test-logic.spec.ts:251:vi.mock("@oxytype/typing-core/result-validity", () => ({
+./frontend/__tests__/test/result.spec.ts:89:vi.mock("@oxytype/typing-core/events/stats", () => ({
+./frontend/__tests__/test/events/stats.spec.ts:75:} from "@oxytype/typing-core/events/helpers";
+./frontend/__tests__/test/events/stats.spec.ts:95:} from "@oxytype/typing-core/events/stats";
+./frontend/__tests__/test/events/stats.spec.ts:102:} from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/events/helpers.spec.ts:14:} from "@oxytype/typing-core/events/helpers";
+./frontend/__tests__/test/events/helpers.spec.ts:16:import type { InputEvent } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/events/data.spec.ts:22:} from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/events/data.spec.ts:24:import { getEventsPerWord } from "@oxytype/typing-core/events/helpers";
+./frontend/__tests__/test/events/anticheat.spec.ts:13:import { calculateWpm } from "@oxytype/typing-core/stats-math";
+./frontend/__tests__/test/events/anticheat.spec.ts:17:} from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/events/anticheat.spec.ts:31:} from "@oxytype/typing-core/events/stats";
+./frontend/__tests__/test/characterization/replay.spec.ts:4:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/test/characterization/lifecycle.spec.ts:3:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/components/pages/test/result/ResultWordsHistory.spec.tsx:32:vi.mock("@oxytype/typing-core/events/stats", () => ({
+./frontend/__tests__/components/pages/test/result/ResultReplay.spec.tsx:1:import type { EventLog } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/components/pages/test/result/ResultReplay.spec.tsx:32: const { getReplaySteps } = await import("@oxytype/typing-core/replay");
+./frontend/__tests__/components/pages/test/result/ResultReplay.spec.tsx:50:vi.mock("@oxytype/typing-core/events/helpers", async (importOriginal) => ({
+./frontend/__tests__/components/pages/test/result/ResultReplay.spec.tsx:52: typeof import("@oxytype/typing-core/events/helpers")
+./frontend/__tests__/components/pages/test/result/ResultReplay.spec.tsx:56:vi.mock("@oxytype/typing-core/events/stats", () => ({
+./frontend/__tests__/components/pages/test/result/ResultChart.spec.tsx:16:vi.mock("@oxytype/typing-core/events/stats", () => ({
+./frontend/__tests__/queries/quote-ownership.spec.ts:2:import type { Quote } from "@oxytype/typing-core/quotes";
+./frontend/__tests__/components/modals/event-log-viewer/video-sync.spec.ts:21:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/components/modals/event-log-viewer/timeline-lanes.spec.ts:18:} from "@oxytype/typing-core/events/types";
+./frontend/__tests__/components/modals/event-log-viewer/scatter-points.spec.ts:15:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+./frontend/__tests__/components/modals/event-log-viewer/event-log.spec.ts:13:import type { TestEventNoMs } from "@oxytype/typing-core/events/types";
+```
+
+## Fixture readers
+
+```text
+packages/typing-core/README.md:72:`__fixtures__/keystrokes` contains raw input/timings, event logs and result
+packages/typing-core/__test__/words-generator.spec.ts:126: "../../__fixtures__/words",
+backend/scripts/anticheat-calibrate.ts:25: ["recorded-web", "packages/typing-core/__fixtures__/keystrokes"],
+backend/scripts/anticheat-calibrate.ts:27: ["", "packages/typing-core/__fixtures__/anticheat"],
+packages/typing-core/__test__/fixtures.ts:11: "../__fixtures__/keystrokes",
+packages/typing-core/scripts/record-fixtures.ts:378:const outDir = join(here, "../__fixtures__/keystrokes");
+packages/typing-core/scripts/record-bot-fixtures.ts:10: * vp fmt packages/typing-core/__fixtures__/anticheat
+packages/typing-core/scripts/record-bot-fixtures.ts:239: "../__fixtures__/anticheat",
+packages/typing-core/__test__/completed-event.spec.ts:28: "../../__fixtures__/keystrokes",
+backend/__tests__/anticheat/detectors.spec.ts:17: "../../../packages/typing-core/__fixtures__",
+frontend/__tests__/test/words-generator-parity.spec.ts:7:// (packages/typing-core/__fixtures__/words/cases.json) runs with a seeded
+frontend/__tests__/test/words-generator-parity.spec.ts:105: "../../../../packages/typing-core/__fixtures__/words",
+frontend/__tests__/test/characterization/replay.spec.ts:36: "../../../../../packages/typing-core/__fixtures__/keystrokes/words-10-errors.json",
+frontend/__tests__/components/modals/LastSignedOutResultModal.spec.tsx:45:import fixture from "../../../../packages/typing-core/__fixtures__/keystrokes/words-10-clean.json";
+```
+
+## Browser rule paths
+
+All six `frontend/src/ts/input/handlers/*.ts` require migration:
+insertion overrides/normalization, deletion penalties, progression, finish/failure,
+beforeinput gates/limits, confidence/freedom, code unindent, physical keys/modifiers.
+Helpers `word-navigation`, `fail-or-finish`, `validation`, `util` also own rules.
+Composition lifecycle, textarea sentinel, Firefox deletion, layout overflow, visible
+word windows and key translation are host observations, explicitly supplied to Rust.
+
+Other consumers: runtime initialization/test-logic/test-timer/completion.client,
+words-generator, funbox hooks, pace, weak-spot, practice, today-tracker, replay,
+result/chart-data/history/accuracy, event-log viewer and result components.
+
+## Funboxes
+
+Inventory identifiers/properties/forced config in `packages/funbox/src/list.ts`.
+Rule hooks in `funbox-word-functions.ts` and `words-generator/funboxes.ts`;
+browser hooks in runtime `funbox/funbox-functions.ts`.
+Rule hooks move to Rust; visual/audio effects, fetch and notifications stay host.
+
+## Reference provenance
+
+SHA-256 of all current fixtures: `reference-manifest.json`. Never regenerate
+snapshots/hashes to make a port pass. Existing backend anticheat consumers retained.
+
diff --git a/docs/rust-engine/progress.md b/docs/rust-engine/progress.md
index 2421ed6e2..6cb95b86d 100644
--- a/docs/rust-engine/progress.md
+++ b/docs/rust-engine/progress.md
@@ -12,14 +12,14 @@ empty commits to inflate count. Do not publish production or releases implicitly
## Current position
-Stage 1, Phase 1.1 in progress. Read root AGENTS.md and the complete migration
+Stage 1, Phase 1.1 complete; Phase 1.2 in progress. Read root AGENTS.md and the complete migration
plan before edits. Installed workspace dependencies with frozen lockfile.
-No acceptance gate passed yet. Existing website engine remains authoritative.
+Phase 1.1 gate passed: inventory, feature map, reproducible browser/performance baseline. Existing website engine remains authoritative.
| Phase | Status | Evidence / remaining gate |
| --- | --- | --- |
-| 1.1 Inventory / baselines | Active | Core baseline: 28 files, 349 tests pass. Browser/performance baseline pending. |
-| 1.2 Contracts / fixtures | Pending | Requires inventory and recorded baseline. |
+| 1.1 Inventory / baselines | Complete | See inventory.md, features.md, baseline.md and raw measurements. |
+| 1.2 Contracts / fixtures | Active | Freeze neutral command contract and differential harness. |
| 1.3 Native / browser proof | Pending | Requires frozen contracts. |
| 1.4 Input / lifecycle / timing | Pending | Requires production browser proof. |
| 1.5 Generation / configuration | Pending | Requires contract and proof. |
diff --git a/docs/rust-engine/reference-manifest.json b/docs/rust-engine/reference-manifest.json
new file mode 100644
index 000000000..7b1cdb1b4
--- /dev/null
+++ b/docs/rust-engine/reference-manifest.json
@@ -0,0 +1,94 @@
+{
+ "sourceCommit": "7b1ae05f204be53b5d9f2714f21c6eea08b25935",
+ "sha256": {
+ "packages/typing-core/__fixtures__/anticheat/fixed-interval.json": "49e27ae3010cd6d42517d8001229ff70e4e5bb2311513599de1d13ac94e22778",
+ "packages/typing-core/__fixtures__/anticheat/independent-gaussian.json": "bcc29d7cdaf61974284c0c4bfc993492a7c27d216d0cd6772c14ade8b4527581",
+ "packages/typing-core/__fixtures__/anticheat/independent-lognormal.json": "7a4623459c5def31b98c7548a33dc932b8694f4ae032dffa082955fbe4f7584f",
+ "packages/typing-core/__fixtures__/anticheat/model-hand-coarse-clock.json": "abeda67a6eef503294a9d0584056b1b1604b8c487fc638a3860ddc2b8e9abc6b",
+ "packages/typing-core/__fixtures__/anticheat/model-hand.json": "60a1b44799f5044304a57611d8dc6c62159f7db1a3b149234f229f9a369a4344",
+ "packages/typing-core/__fixtures__/anticheat/ms-jitter.json": "2783156a7aaecd6dfd8e670acff38f0319e70c6772bc22265c14bd49aed7571f",
+ "packages/typing-core/__fixtures__/anticheat/replayed-recording.json": "6bf7a12c15baf67854c539db6e5896bdb243b23b515770672f11b61811773223",
+ "packages/typing-core/__fixtures__/anticheat/scripted-playwright.json": "4b545366bf8a83492b1bfa4b2dbb597dd1e558a1db110e35ddf705b8ab2f1f7f",
+ "packages/typing-core/__fixtures__/anticheat/sub-ms-jitter.json": "e49642c327f70f80e325ba5fd5cac7ae604fdcb2498224b7181dd512aa7b3d07",
+ "packages/typing-core/__fixtures__/anticheat/uniform-range.json": "50ea9e84f812700dc04bb2375e795caca1ce3da27422bb432a31d6718bc204ed",
+ "packages/typing-core/__fixtures__/anticheat/value-pool.json": "2e4dc5275563dabf1fd99ba3bc76ff352f24d20392fea31e8c73fd1cbc617973",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/custom-repeat.json": "0067176be6ef63625e68a3ec9c3e23bda6a27607963dba2c175b17c180659c25",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/quote-short.json": "a12ae9a7b863638b71d325cc948b4bf6bc6d7caa981be8860f52448f5d028b11",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/time-15.json": "a3d815ea3c0b067041f9536b890eb3b9128f7253cf79b6b7d0869ead1d95c933",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/words-10-clean.json": "0bd9ec98683da1b4194c485a8de5fe264708a9d335f05953f2db79a2f5110005",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/words-10-errors.json": "2c6f639663fcd09c06cdde88627f58c8970b414e23ae77ed5ef166db7429fba2",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/words-10-punctuation-numbers.json": "be6733c13da1cccd012c15fc0c95d6adace3fc193750258f0c5ec0de2690bb0c",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/words-10-stop-on-letter.json": "5400a17d7491269e7f3f32dd9e01c8a9a09aaab49569482f265ad01e0af66e36",
+ "packages/typing-core/__fixtures__/keystrokes/__snapshots__/zen.json": "82c02edadfb1cf4dd104c161681a0c8e5807437b058de6e4545dd306977eb6b6",
+ "packages/typing-core/__fixtures__/keystrokes/custom-repeat.json": "f1737b55a17ab65699244ea753b63892867ccedcd801f021469a11f187833699",
+ "packages/typing-core/__fixtures__/keystrokes/quote-short.json": "fd4137dcec65daae0d153cfa55efa6262d1fcf2b41ee371f61ea6bc5e56259d0",
+ "packages/typing-core/__fixtures__/keystrokes/time-15.json": "2748818c903bddac123d623a3224dcc8708172adc04e4a2243b9ad6fa76db1c6",
+ "packages/typing-core/__fixtures__/keystrokes/words-10-clean.json": "80af3b5f5c0b78e51f1bbe87a9f70e8da1139d366b1098c82e6297ee2fa6a0b3",
+ "packages/typing-core/__fixtures__/keystrokes/words-10-errors.json": "439b8fdb7aa00cfac96c51a808e28a4e82e04168edb1f436ebc818b10e0d04f4",
+ "packages/typing-core/__fixtures__/keystrokes/words-10-punctuation-numbers.json": "b531f395292294b34b01a1a7579c28783a040ef999dcd6c182f098de79414a56",
+ "packages/typing-core/__fixtures__/keystrokes/words-10-stop-on-letter.json": "c000ab190fd3f8d1dd3f1fc662bf0cf5dfd36777d3903b0b651969a791612e6e",
+ "packages/typing-core/__fixtures__/keystrokes/zen.json": "d82a6c76fb585a9c074050ae53db465fedb205ed2d0084f308b5389cb0899273",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-newlines-tabs.json": "565eba8c001ac99e03252d644330dc0bf2b7f05ee90adadeb2627e3d7a94dc61",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-random.json": "c6620165e3acd050db4a4389a3a751879d85aac00c5fc42e64da25fd895a06a6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-repeat.json": "68d427aeef4f9a607f240b8edb7c44f3ed011b890edf51d571c3418b27d900eb",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-repeated.json": "2d78795cc96ece06d64b723f026a3e9da14dfbf5f5b3a52de86b3dac3a5a6790",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-sections.json": "28c070d9f4d0af30ae5abdde2931c61b8bd720dc22a6b59a7616cff37b7bbe39",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-shuffle.json": "3e35bf7c10c6f501c25c2a0964566a99bfbfe19cf034a039b92f009da4327128",
+ "packages/typing-core/__fixtures__/words/__snapshots__/custom-time.json": "91222001936a44cbfc3b7ead2215ba5229d2043b406e90b03d7c0b0bcf81a5ce",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-58008.json": "56a86f30ac6b6603ad3dd01501968d27351eb8d83a206f0e2193a85e46527b86",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-ALL_CAPS.json": "bf1c1b9636c1674e642285ca664694c62831a1f9b858ded8108ec27d5637da58",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-IPv4.json": "c3fa29c2b8c1489be1ff6e3d02e2ec132bf97370a7823b1453326df6fa009c0c",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-IPv6.json": "ca89e0fc491b3a49d6a0953c0594cb43d8b0cff9dfd75ead1409231d8926ebe7",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-arrows.json": "88a7b3afbde9ee75fb013fa00cb900bb8a89ce7328b0765726b474b0c591049b",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-ascii.json": "9d6404cb0a01559234a75511050ed4fc0f10c457be271bcbf97c3dfc9465da0b",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-backwards-quote.json": "85ad310b3b1f03b5e733a4ed6be034af9a4ef3070c66620bc68ef61b940ce36f",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-backwards.json": "bd3cc497b3b87b78ccb046b84f69d8efd55538a5bd41c8263dca518028bcac4b",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-binary.json": "1b4d2e7543d3b2d03d3436afd88dfd78797d4983a89895439759493ad656ae99",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-capitals.json": "523a438133c2b0e29fc0bd4a00653154871460258d844129793885191f8ca2b4",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-ddoouubblleedd.json": "2a695ac0034f045667d3efd6174af08fc4b7fdaa0481802b30391f53293c85d3",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-gibberish.json": "fd1097dfcce6bbcdd2b3d2368b50f5cda401c9d1b94fb6055bb4c45b31b9f6bd",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-hexadecimal.json": "45eeab435699b75e5fbdcb4d8e0fc53d73ae087616cc2fd75b04fc4b7b60c7ea",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-instant_messaging.json": "c3519cf54b63fa91010bcb25ed8808bee9264eeccc833fa5b687621447d1ab17",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-morse.json": "6286f6b6c62ab18160a6f6b7674e1f43bd8b79cec6b6683d674d9882b78dd14e",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-nospace.json": "e4f001ccb475a0f56f98d822abdb68e229b81bd1253341e3b6f057c702ba8c7e",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-plus_one.json": "4779a76d486989551a559e30639602359c96064a8e298a96f973cdbabc59aff6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-plus_three.json": "e2eb89d5e0ad9969f8e2c01966283d6e1bfab644a379be26fbc77a20f45944d6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-polyglot.json": "1ac08bd09a0e075af32e2d8a09f2c802819d0e2268056076648f196b59fa7668",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-pseudolang.json": "3e6b8ba74aa42118db1e69069bb3951fef895194f654d872dd1865e5bd07eaa6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-rAnDoMcAsE.json": "ad9c60843cb1af5a5e7c4b768ee573d66413a6216b2f9ec487f2d8e20c295f37",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-rot13-capitals-time.json": "b0873b969918b510671d063031ad5386f24551afe41878974c21bc8e7a40cd36",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-rot13.json": "95c9ec480cb193fadbb2da7477a12bc495be59a96c8056d8b7b7a11080fa7b0c",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-sPoNgEcAsE.json": "a85fb44d7105ef305f2badeff10b4d9e8df0bcb4f657f50082ee7d9adb8f2162",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-specials.json": "1e09f3ed31bd08160b47bd8ad39e61a58501d0ec119382617005d6cac7abf202",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-underscore_spaces.json": "fdbdd1e47411ee371e33382e6cbd5c69ff234cd068c9d0983361bd82f70fd922",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-weakspot.json": "9da4a413a1b0d7a504277cc64847ce08f062a70896cd078777a230df53b628d3",
+ "packages/typing-core/__fixtures__/words/__snapshots__/funbox-zipf.json": "34814f4c7cdaa650fe040e5bd212e1ee5c2e5e4b56a99e9c90a9807efbbbed59",
+ "packages/typing-core/__fixtures__/words/__snapshots__/quote-all.json": "89e74de559900c8426693b3665b81c5470616a29d0354a7dfc8f2dd888387f71",
+ "packages/typing-core/__fixtures__/words/__snapshots__/quote-british-english.json": "849d721de7e377dd6a8e2384be0228d79ce6f5d696574ddadf084ab1765240cc",
+ "packages/typing-core/__fixtures__/words/__snapshots__/quote-by-id.json": "2b298c10f489044626e0e1f5df598257b9b9f2eba4522597ddebed8cd2987d45",
+ "packages/typing-core/__fixtures__/words/__snapshots__/quote-repeated.json": "f00eb1986a48e5ac305578814a549f7baddff3ec191e61f2f5398173f3a03965",
+ "packages/typing-core/__fixtures__/words/__snapshots__/quote-short.json": "e19f7bd17071c47892c2dbb0ab7dee5bd947e434b9794d658cc9d97e7894c1e6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/time-15-english-1k.json": "51a46c8616aac29bb55cdf11cae8fad7fc1d6bbbd36925e2206fc5ea12415d12",
+ "packages/typing-core/__fixtures__/words/__snapshots__/time-30.json": "426cd3b6d1e16dc2ade7436931f2eb22ab5c82867f0578ba2147f4bf0c59fa56",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-0-infinite.json": "62a0c9f4a03407693012e805874db1ca95a21ac2b2150b7b64f4568e8994e313",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-10.json": "836409db9c33a425cc1e072fe11ab113535316f5725111aa18a1ff04fba2089b",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-100-show-all-lines.json": "391e260eca1fde988198f85809361c75e5f0edcc9d1bd6fecdbfa3cde4badb5f",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-20-repeated.json": "df9e3d128a9fadbcec435bc24001d842136cfd78d22858f2c11661df9382d0c5",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-25-numbers.json": "ff9b4379b0539b883ad970466ad0c80b4122419adf4af4d7abba327f0ce662b4",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-25-punctuation.json": "0b7e8f75247b462e43c8dc1e16712e843f68241ca10c208da8050b22f9a1030a",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-arabic-numbers.json": "d24d0ab9599cba6405492137a626f1f0931df7ec09ab34d582a09e85d1c3ffc5",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-code-javascript-punctuation.json": "50c97aa8b875fad4ba184c5581b0f626eaa8154dfb05a8b07b2767cb8a6eae5e",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-french-punctuation.json": "9cc12784ca7412ac3306888ac688e5f3e760c7f86b5087def63b3df3ac9c388b",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-german.json": "63798e2fc2642830187a6ac075791bc05b7de822808fa4e89f3568b5f736d401",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-lazy-french.json": "a717093b083c611f53390ef24b311bb515c41ea66366227fb841a11c2e3b66ea",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-lazy-polish-punctuation.json": "5361754e5f1616d988a2eb54cbdffd6d2650a7994aa4939839b04a7ef4905267",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-spanish-punctuation.json": "987f3d4f7c3c9616dcd494213d1755870b184dfcb3f1481a9e005099c1b8fac5",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-swiss-german.json": "921171aa48a1988c6bb2103036b71dddeab4a224715ddd49a29bb763fd525f26",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-30-turkish-punctuation.json": "d5c5ec3450530d987ba6d4268c549c6e8cd0970b3afc3cbaefb589df58472fe6",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-50-british-english.json": "8073ca58f8da6f91e763cbca20459f46291154f3f59b05ef51982a0dd23e603f",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-50-english-punctuation-contractions.json": "3ad490a62ac90349dc366e326689fdfc39c707784abb77cd269b95e4d822a83a",
+ "packages/typing-core/__fixtures__/words/__snapshots__/words-50-punctuation-numbers.json": "06467c350027e18b51f14024bc3533854defd39e5ccc4ce8f3116876241a195a",
+ "packages/typing-core/__fixtures__/words/__snapshots__/zen.json": "5589ce2c71d92f84583d8af5b0bcc2496124582b2856a6db9772a19a22e7e3dd",
+ "packages/typing-core/__fixtures__/words/cases.json": "c92f32144415228ea531c7beb517bc61ea8f239565153ec962278f3cd8400942"
+ }
+}
diff --git a/frontend/e2e/engine-performance.migration.ts b/frontend/e2e/engine-performance.migration.ts
new file mode 100644
index 000000000..74ab85e76
--- /dev/null
+++ b/frontend/e2e/engine-performance.migration.ts
@@ -0,0 +1,87 @@
+import { writeFile } from "node:fs/promises";
+import { expect, test } from "playwright/test";
+
+// Same built Worker fixture as the website regressions. Freeze before porting.
+test("typing engine baseline: input paint, restart, retained heap", async ({
+ page,
+ context,
+}) => {
+ const output = process.env["ENGINE_PERFORMANCE_OUTPUT"];
+ if (output === undefined) throw new Error("Set ENGINE_PERFORMANCE_OUTPUT");
+ await page.addInitScript(() => {
+ localStorage.setItem(
+ "acceptedCookies",
+ JSON.stringify({ security: false, analytics: false, sentry: false }),
+ );
+ localStorage.setItem(
+ "config",
+ JSON.stringify({ mode: "zen", smoothCaret: "off", quickRestart: "off" }),
+ );
+ });
+ await page.goto("/");
+ await expect(page.locator("#typingTest")).toHaveCSS("opacity", "1");
+ await page.locator("#words").click();
+ const cdp = await context.newCDPSession(page);
+ await cdp.send("Performance.enable");
+ await cdp.send("HeapProfiler.collectGarbage");
+ const initialHeap = (await cdp.send("Runtime.getHeapUsage")).usedSize;
+ const inputToPaint: number[] = [];
+ const restart: number[] = [];
+ for (let sample = 0; sample < 20; sample++) {
+ // Capture host beforeinput time; include rendering through the next frame.
+ await page.evaluate(() => {
+ const input = document.querySelector("#wordsInput");
+ if (input === null) throw new Error("Missing input");
+ input.addEventListener(
+ "beforeinput",
+ () => performance.mark("engine-input"),
+ { once: true },
+ );
+ });
+ await page.keyboard.type("a");
+ await expect(page.locator("#wordsInput")).toHaveValue(" a");
+ inputToPaint.push(
+ await page.evaluate(async () => {
+ await new Promise((resolve) =>
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
+ );
+ return (
+ performance.now() -
+ (performance.getEntriesByName("engine-input").at(-1)?.startTime ??
+ performance.now())
+ );
+ }),
+ );
+ const start = performance.now();
+ await page.locator("#restartTestButton").click();
+ await expect(page.locator("#typingTest")).toHaveCSS("opacity", "1");
+ await expect(page.locator("#wordsInput")).toHaveValue(" ");
+ restart.push(performance.now() - start);
+ await page.locator("#words").click();
+ }
+ // Real keyboard observations; no engine-internal shortcut for the long run.
+ await page.keyboard.type("a ".repeat(1000));
+ await cdp.send("HeapProfiler.collectGarbage");
+ const activeHeap = (await cdp.send("Runtime.getHeapUsage")).usedSize;
+ await page.locator("#restartTestButton").click();
+ await expect(page.locator("#typingTest")).toHaveCSS("opacity", "1");
+ await cdp.send("HeapProfiler.collectGarbage");
+ const resetHeap = (await cdp.send("Runtime.getHeapUsage")).usedSize;
+ const assets = await page.evaluate(() =>
+ performance
+ .getEntriesByType("resource")
+ .filter((entry) => /\.(js|wasm)(\?|$)/.test(entry.name))
+ .map((entry) => {
+ const resource = entry as PerformanceResourceTiming;
+ return {
+ name: new URL(resource.name).pathname,
+ bytes: resource.decodedBodySize,
+ transfer: resource.transferSize,
+ };
+ }),
+ );
+ await writeFile(
+ output,
+ `${JSON.stringify({ browser: context.browser()?.version(), fixture: "built Start Worker; desktop Chromium; zen; 20 input/restart samples; double-rAF paint; 2000 keyboard characters; forced GC", inputToPaint, restart, initialHeap, activeHeap, resetHeap, assets }, null, 2)}\n`,
+ );
+});
From e5a1c398c542c62de9587f81a6898d58a992bfd8 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Thu, 22 Oct 2026 23:50:45 +0530
Subject: [PATCH 03/93] test(engine): preserve immutable fixtures in neutral
package
---
packages/typing-fixtures/README.md | 12 +
packages/typing-fixtures/package.json | 6 +
.../reference/anticheat/fixed-interval.json | 50 +
.../anticheat/independent-gaussian.json | 79 +
.../anticheat/independent-lognormal.json | 78 +
.../anticheat/model-hand-coarse-clock.json | 99 +
.../reference/anticheat/model-hand.json | 193 ++
.../reference/anticheat/ms-jitter.json | 78 +
.../anticheat/replayed-recording.json | 205 ++
.../anticheat/scripted-playwright.json | 55 +
.../reference/anticheat/sub-ms-jitter.json | 82 +
.../reference/anticheat/uniform-range.json | 78 +
.../reference/anticheat/value-pool.json | 51 +
.../__snapshots__/custom-repeat.json | 108 +
.../keystrokes/__snapshots__/quote-short.json | 122 +
.../keystrokes/__snapshots__/time-15.json | 128 +
.../__snapshots__/words-10-clean.json | 99 +
.../__snapshots__/words-10-errors.json | 110 +
.../words-10-punctuation-numbers.json | 102 +
.../words-10-stop-on-letter.json | 112 +
.../keystrokes/__snapshots__/zen.json | 70 +
.../reference/keystrokes/custom-repeat.json | 1645 ++++++++++
.../reference/keystrokes/quote-short.json | 2523 +++++++++++++++
.../reference/keystrokes/time-15.json | 2856 +++++++++++++++++
.../reference/keystrokes/words-10-clean.json | 1560 +++++++++
.../reference/keystrokes/words-10-errors.json | 2152 +++++++++++++
.../words-10-punctuation-numbers.json | 1855 +++++++++++
.../keystrokes/words-10-stop-on-letter.json | 2168 +++++++++++++
.../reference/keystrokes/zen.json | 1106 +++++++
.../__snapshots__/custom-newlines-tabs.json | 10 +
.../words/__snapshots__/custom-random.json | 44 +
.../words/__snapshots__/custom-repeat.json | 20 +
.../words/__snapshots__/custom-repeated.json | 23 +
.../words/__snapshots__/custom-sections.json | 25 +
.../words/__snapshots__/custom-shuffle.json | 21 +
.../words/__snapshots__/custom-time.json | 144 +
.../words/__snapshots__/funbox-58008.json | 33 +
.../words/__snapshots__/funbox-ALL_CAPS.json | 33 +
.../words/__snapshots__/funbox-IPv4.json | 33 +
.../words/__snapshots__/funbox-IPv6.json | 33 +
.../words/__snapshots__/funbox-arrows.json | 33 +
.../words/__snapshots__/funbox-ascii.json | 33 +
.../__snapshots__/funbox-backwards-quote.json | 31 +
.../words/__snapshots__/funbox-backwards.json | 33 +
.../words/__snapshots__/funbox-binary.json | 33 +
.../words/__snapshots__/funbox-capitals.json | 33 +
.../__snapshots__/funbox-ddoouubblleedd.json | 33 +
.../words/__snapshots__/funbox-gibberish.json | 33 +
.../__snapshots__/funbox-hexadecimal.json | 33 +
.../funbox-instant_messaging.json | 33 +
.../words/__snapshots__/funbox-morse.json | 33 +
.../words/__snapshots__/funbox-nospace.json | 33 +
.../words/__snapshots__/funbox-plus_one.json | 10 +
.../__snapshots__/funbox-plus_three.json | 10 +
.../words/__snapshots__/funbox-polyglot.json | 44 +
.../__snapshots__/funbox-pseudolang.json | 33 +
.../__snapshots__/funbox-rAnDoMcAsE.json | 33 +
.../funbox-rot13-capitals-time.json | 144 +
.../words/__snapshots__/funbox-rot13.json | 33 +
.../__snapshots__/funbox-sPoNgEcAsE.json | 33 +
.../words/__snapshots__/funbox-specials.json | 33 +
.../funbox-underscore_spaces.json | 33 +
.../words/__snapshots__/funbox-weakspot.json | 33 +
.../words/__snapshots__/funbox-zipf.json | 33 +
.../words/__snapshots__/quote-all.json | 28 +
.../__snapshots__/quote-british-english.json | 26 +
.../words/__snapshots__/quote-by-id.json | 29 +
.../words/__snapshots__/quote-repeated.json | 30 +
.../words/__snapshots__/quote-short.json | 31 +
.../__snapshots__/time-15-english-1k.json | 118 +
.../words/__snapshots__/time-30.json | 169 +
.../words/__snapshots__/words-0-infinite.json | 144 +
.../words/__snapshots__/words-10.json | 21 +
.../words-100-show-all-lines.json | 171 +
.../__snapshots__/words-20-repeated.json | 33 +
.../words/__snapshots__/words-25-numbers.json | 39 +
.../__snapshots__/words-25-punctuation.json | 39 +
.../words-30-arabic-numbers.json | 45 +
.../words-30-code-javascript-punctuation.json | 44 +
.../words-30-french-punctuation.json | 44 +
.../words/__snapshots__/words-30-german.json | 44 +
.../__snapshots__/words-30-lazy-french.json | 44 +
.../words-30-lazy-polish-punctuation.json | 44 +
.../words-30-spanish-punctuation.json | 44 +
.../__snapshots__/words-30-swiss-german.json | 44 +
.../words-30-turkish-punctuation.json | 44 +
.../words-50-british-english.json | 65 +
...s-50-english-punctuation-contractions.json | 65 +
.../words-50-punctuation-numbers.json | 65 +
.../reference/words/__snapshots__/zen.json | 10 +
.../reference/words/cases.json | 687 ++++
91 files changed, 21234 insertions(+)
create mode 100644 packages/typing-fixtures/README.md
create mode 100644 packages/typing-fixtures/package.json
create mode 100644 packages/typing-fixtures/reference/anticheat/fixed-interval.json
create mode 100644 packages/typing-fixtures/reference/anticheat/independent-gaussian.json
create mode 100644 packages/typing-fixtures/reference/anticheat/independent-lognormal.json
create mode 100644 packages/typing-fixtures/reference/anticheat/model-hand-coarse-clock.json
create mode 100644 packages/typing-fixtures/reference/anticheat/model-hand.json
create mode 100644 packages/typing-fixtures/reference/anticheat/ms-jitter.json
create mode 100644 packages/typing-fixtures/reference/anticheat/replayed-recording.json
create mode 100644 packages/typing-fixtures/reference/anticheat/scripted-playwright.json
create mode 100644 packages/typing-fixtures/reference/anticheat/sub-ms-jitter.json
create mode 100644 packages/typing-fixtures/reference/anticheat/uniform-range.json
create mode 100644 packages/typing-fixtures/reference/anticheat/value-pool.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/custom-repeat.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/quote-short.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/time-15.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-clean.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-errors.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-punctuation-numbers.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-stop-on-letter.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/__snapshots__/zen.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/custom-repeat.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/quote-short.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/time-15.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/words-10-clean.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/words-10-errors.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/words-10-punctuation-numbers.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/words-10-stop-on-letter.json
create mode 100644 packages/typing-fixtures/reference/keystrokes/zen.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-newlines-tabs.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-random.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-repeat.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-repeated.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-sections.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-shuffle.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/custom-time.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-58008.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-ALL_CAPS.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv4.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv6.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-arrows.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-ascii.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-backwards-quote.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-backwards.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-binary.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-capitals.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-ddoouubblleedd.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-gibberish.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-hexadecimal.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-instant_messaging.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-morse.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-nospace.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-plus_one.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-plus_three.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-polyglot.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-pseudolang.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-rAnDoMcAsE.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-rot13-capitals-time.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-rot13.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-sPoNgEcAsE.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-specials.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-underscore_spaces.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-weakspot.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/funbox-zipf.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/quote-all.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/quote-british-english.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/quote-by-id.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/quote-repeated.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/quote-short.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/time-15-english-1k.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/time-30.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-0-infinite.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-10.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-100-show-all-lines.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-20-repeated.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-25-numbers.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-25-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-arabic-numbers.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-code-javascript-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-french-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-german.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-french.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-polish-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-spanish-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-swiss-german.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-30-turkish-punctuation.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-50-british-english.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-50-english-punctuation-contractions.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/words-50-punctuation-numbers.json
create mode 100644 packages/typing-fixtures/reference/words/__snapshots__/zen.json
create mode 100644 packages/typing-fixtures/reference/words/cases.json
diff --git a/packages/typing-fixtures/README.md b/packages/typing-fixtures/README.md
new file mode 100644
index 000000000..aa2b5250c
--- /dev/null
+++ b/packages/typing-fixtures/README.md
@@ -0,0 +1,12 @@
+# Typing reference fixtures
+
+`reference/` is a byte-for-byte copy of `packages/typing-core/__fixtures__/` at
+`7b1ae05f204be53b5d9f2714f21c6eea08b25935`. Preserve original files and provenance;
+never regenerate these snapshots to make a new implementation pass.
+
+`commands/` holds explicit config/content/time/command traces. Text fields are
+UTF-16 unit arrays, permitting isolated surrogate halves. `oracle/` holds frozen
+TypeScript output at every command. New captures refuse to overwrite output.
+
+See `docs/rust-engine/contract.md` and `reference-manifest.json`. Fixture schemas
+remain data contracts, independent of either engine implementation.
diff --git a/packages/typing-fixtures/package.json b/packages/typing-fixtures/package.json
new file mode 100644
index 000000000..5f0e5e1df
--- /dev/null
+++ b/packages/typing-fixtures/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@oxytype/typing-fixtures",
+ "private": true,
+ "description": "Immutable language-neutral typing engine reference data",
+ "license": "GPL-3.0"
+}
diff --git a/packages/typing-fixtures/reference/anticheat/fixed-interval.json b/packages/typing-fixtures/reference/anticheat/fixed-interval.json
new file mode 100644
index 000000000..9f1a06d6d
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/fixed-interval.json
@@ -0,0 +1,50 @@
+{
+ "name": "fixed-interval",
+ "label": "bot",
+ "client": "web",
+ "description": "Every gap 80 ms and every hold 45 ms.",
+ "samples": [
+ {
+ "keySpacing": [
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80,
+ 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80
+ ],
+ "keyDuration": [
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
+ 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/independent-gaussian.json b/packages/typing-fixtures/reference/anticheat/independent-gaussian.json
new file mode 100644
index 000000000..c9fc20a58
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/independent-gaussian.json
@@ -0,0 +1,79 @@
+{
+ "name": "independent-gaussian",
+ "label": "bot",
+ "client": "web",
+ "description": "Independent normal gaps (85±18 ms) and holds (45±9 ms).",
+ "samples": [
+ {
+ "keySpacing": [
+ 60.51, 75.7, 67.3, 85.8, 104.51, 103.59, 88.54, 118.04, 97.95, 120.4,
+ 66.69, 87.05, 41.17, 59.22, 58.05, 108.58, 82.14, 76.74, 85.44, 79.06,
+ 107.9, 112.54, 69.28, 43.51, 97.38, 104.18, 45.62, 93.75, 103.13,
+ 123.02, 88.88, 132.91, 89.94, 62.07, 43.79, 76.76, 85.93, 84, 87.52, 76,
+ 100.46, 107.81, 70.14, 105.72, 59.24, 80.31, 68.47, 71.12, 91.77, 82.43,
+ 88.55, 69.09, 110.14, 43.28, 74.51, 98.5, 85.83, 95.45, 66.24, 83.15,
+ 56.35, 108.7, 90.34, 101.64, 81.43, 43.66, 77.91, 68.87, 106.57, 70.1,
+ 88.5, 100.57, 76.78, 89.45, 63.75, 89.63, 73.61, 84.6, 89.57, 114.09,
+ 86.99, 102.16, 144.33, 67.13, 96.38, 90.17, 78.24, 98.57, 69.04, 86.71,
+ 44.87, 61.28, 74.42, 78.11, 100.19, 90.78, 77.69, 98.16, 96.61, 76.96,
+ 67.99, 94.75, 81.61, 85.6, 88.73, 102.23, 89.96, 78.48, 42.62, 69.55,
+ 83.13, 63.96, 92.63, 112.78, 89.6, 82.64, 76.08, 73.91, 100.36, 112.4,
+ 74.94, 103.01, 62.69, 52.11, 90.04, 86.16, 88.84, 82.35, 70.62, 85.8,
+ 83.62, 69.55, 80.25, 100.57, 94.49, 101.41, 106.39, 62.91, 71.79,
+ 112.88, 66.15, 122, 66.33, 89.3, 87.53, 89.59, 54.08, 95.79, 89.46,
+ 49.7, 61.96, 113.11, 79.67, 103.18, 91.92, 94.43, 106.18, 85.6, 82.18,
+ 110.27, 78.82, 67.27, 83.52, 94.95, 78.23, 96.7, 69.85, 82.96, 97.88,
+ 49.5, 67.01, 96.09, 103.17, 102.09, 70.63, 80.82, 97.37, 68.53, 123.78,
+ 77, 58.34, 80.8, 94.38, 94.56, 95.01, 115.07, 74.77, 76.21, 105.16,
+ 88.7, 64.11, 104.13, 79.35, 48.89, 75, 70.57, 63.76, 103.61, 57.08,
+ 131.26, 74.21, 74.54, 87.03, 92.26, 110.97, 68.16, 68.23, 87.85, 81.87,
+ 107.44, 88.75, 108.91, 119.07, 90.48, 101.91, 82.97, 90.79, 85.96,
+ 70.01, 64.17, 95.16, 73.75, 76.99, 69.55, 94.35, 89.85, 90.14, 84.67,
+ 68.81, 81.32, 93.97, 60.7, 63, 110.36, 80.98, 65.45, 84.32, 84.97,
+ 108.04, 79.91, 81.64, 121.91, 92.05, 35.97, 88.57, 74.13, 98.65, 80.87,
+ 70.65, 81.86, 82.72, 68.26, 82.74, 93.28, 79.63, 102.77, 96.64, 76.24,
+ 60, 84.84, 60.19, 124.56, 73.61, 103.49, 61.04, 79.75, 76.23, 92.71,
+ 61.56, 91.46, 95.36, 86.3, 73.55, 93.66, 94.02, 68.55, 95.02, 102.82,
+ 101.62, 66.62, 97.41, 41.16, 120.4, 77.91, 72.57, 119.41, 84.49, 91.43,
+ 80.24, 76.16, 103.53, 70.09, 87, 112.05, 75.06, 101.81, 66.26, 51.98,
+ 82.3, 66.8, 88.88, 86.09, 89.14, 92.07, 64.84, 117.42, 72.1, 65.05,
+ 80.45, 103.57, 78.17, 114.88, 88.62, 73.19, 90.02, 99.22, 85.61, 72.66,
+ 108.94
+ ],
+ "keyDuration": [
+ 40.99, 45.19, 38.23, 45.69, 35.01, 49.47, 26.15, 46.48, 40.45, 51.58,
+ 42.92, 45.28, 37.04, 41.68, 45.53, 42.33, 57.81, 37.72, 56.44, 48.92,
+ 44.95, 49.05, 48.23, 52.55, 60.42, 25.87, 55.45, 47.28, 41.58, 50.52,
+ 54.62, 42.41, 48.05, 52.36, 45.69, 37.26, 28.56, 56.24, 45.63, 40.89,
+ 48.57, 56.12, 53.54, 60.07, 38.58, 61.88, 60.29, 44.01, 23.47, 52.23,
+ 44.56, 41.11, 31.12, 49.47, 42.04, 49.38, 41.48, 55.78, 41.36, 46.42,
+ 61.55, 46.71, 41.18, 40.68, 27.2, 43.07, 52.13, 38.82, 51.87, 47.1,
+ 58.01, 42.01, 36.77, 38.14, 30.62, 37.55, 41.2, 44.7, 48.34, 41.64,
+ 49.35, 34.89, 51.71, 47.75, 51.05, 55.04, 45.23, 31.87, 42.17, 45.7,
+ 62.42, 21.8, 39.27, 27.77, 47.92, 44.8, 36.4, 35.94, 45.95, 50.02,
+ 31.05, 36.12, 34.22, 52.57, 52.79, 57.36, 55.14, 42.6, 41.02, 64.72,
+ 48.8, 51.13, 43.19, 63.56, 52.63, 16.45, 30.58, 41.63, 32.8, 52.92,
+ 48.48, 48.52, 46.42, 35.66, 51.21, 44.55, 23.7, 30.59, 48.34, 43.7,
+ 38.56, 43.12, 40.93, 46.01, 48.19, 40.1, 46.13, 37.43, 57.7, 49.39,
+ 40.42, 40.81, 50.84, 42.3, 60.85, 50.06, 43.64, 57.42, 42.68, 55.53,
+ 53.45, 54.58, 47.41, 57.5, 35.12, 44.52, 23.69, 44.66, 27.38, 41.97,
+ 47.72, 56.42, 46.93, 48.71, 42.81, 43.15, 28.58, 54.98, 28.16, 43.69,
+ 52.51, 42.7, 32.17, 48.78, 33.88, 58.97, 44.3, 53.78, 48.09, 45.27,
+ 28.45, 43.96, 49.15, 36.48, 44.6, 39.39, 33.78, 40.91, 27.57, 48.09,
+ 41.21, 48.75, 53.63, 48.25, 39.3, 40.59, 61.38, 39.71, 52.69, 34.03,
+ 47.04, 37.62, 36.57, 31.88, 45.06, 28.17, 44.57, 55.95, 47.8, 54.04,
+ 31.06, 29.64, 56.44, 45.18, 53.54, 36.94, 44.66, 64.7, 62.02, 50.33,
+ 35.25, 41.27, 48.04, 35.34, 38.03, 32.57, 52.36, 50.83, 39.54, 41.53,
+ 44.86, 49.47, 44.29, 49.4, 48.99, 41.44, 44.63, 47.38, 37.67, 42.42,
+ 51.57, 42.27, 44.03, 69.01, 66.36, 37.14, 54.93, 40.49, 50.59, 34.68,
+ 53.36, 38.55, 44.77, 39.57, 52.12, 46.94, 39.57, 32.35, 49.31, 50.2,
+ 32.02, 50.11, 52.82, 38.8, 40.05, 54.07, 43.18, 52.92, 69.88, 50.59,
+ 37.93, 49.81, 35.46, 35.71, 47.81, 51.67, 48.23, 42.38, 42.36, 48,
+ 42.04, 42.57, 51.42, 40.15, 32.72, 46.4, 33.62, 47.52, 34.99, 29.23,
+ 32.4, 48.2, 48.95, 60.35, 42.1, 46.79, 43.03, 31.17, 43.48, 47.54,
+ 54.74, 55.6, 53.21, 41.91, 44.6, 60.67, 46.12, 49.92, 58.05, 48.47,
+ 45.91, 44.04, 50.84, 38.08, 34.78, 36.68, 54.4, 51.65, 35.4, 39.57
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/independent-lognormal.json b/packages/typing-fixtures/reference/anticheat/independent-lognormal.json
new file mode 100644
index 000000000..844a9d608
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/independent-lognormal.json
@@ -0,0 +1,78 @@
+{
+ "name": "independent-lognormal",
+ "label": "bot",
+ "client": "web",
+ "description": "Human-shaped lognormal gaps and holds, but every draw independent.",
+ "samples": [
+ {
+ "keySpacing": [
+ 110.2, 103.56, 73.09, 122.86, 53.21, 64.39, 124.28, 75.45, 66.2, 104.59,
+ 68.52, 84.74, 70.05, 135.49, 47.22, 109.91, 75.96, 130.83, 70.14, 74.46,
+ 109.57, 94.9, 68.38, 88.67, 63.08, 66.41, 93.09, 72.82, 61.23, 105.16,
+ 90.51, 97.33, 67.34, 79.48, 83.02, 65.48, 85.26, 76.78, 71.25, 72.68,
+ 93.27, 43.92, 69.65, 74.55, 92.06, 74.83, 107.72, 95.03, 51.85, 87.02,
+ 117.71, 106.08, 109.51, 121.3, 97.74, 99.47, 114.8, 90.93, 118.86,
+ 110.49, 66.61, 123.85, 87.55, 62.75, 70.58, 94.33, 64.12, 94.57, 196.44,
+ 91.36, 91.75, 92.44, 52.56, 88.39, 54.88, 104.14, 37.26, 71.55, 65,
+ 89.11, 62.42, 114.34, 43.93, 103.15, 107.6, 109.3, 93.7, 80.62, 97.92,
+ 108.76, 98.08, 97.55, 73.44, 61.81, 91.73, 60.24, 82.94, 82.08, 43.82,
+ 57.45, 76.77, 55.74, 81.49, 89.04, 84.06, 83.17, 53.73, 127.74, 85.81,
+ 138.84, 96.76, 136.56, 61.19, 80.97, 78.56, 68.24, 99.13, 64.64, 64.97,
+ 80.45, 97.54, 87.96, 121.07, 65.15, 98.31, 70.38, 107.92, 84.88, 91.48,
+ 73.18, 37.34, 77.47, 72.16, 64.25, 91.89, 69.66, 117.35, 55.26, 64.51,
+ 139.94, 62.84, 57.86, 80.88, 86.22, 81.76, 57.19, 69.56, 95.6, 99.37,
+ 83.85, 87.82, 87.89, 110.22, 65.16, 39.12, 48.58, 56.21, 79.41, 109.26,
+ 55.25, 75.6, 73.53, 67.26, 67.26, 52.37, 126.35, 78.8, 98.56, 60.87,
+ 122.26, 72.85, 62.89, 45.72, 57.77, 50.6, 75.12, 112.57, 68.91, 92.6,
+ 79.83, 51.15, 72.8, 114.21, 96.61, 63.99, 109.89, 98.41, 85.23, 93.55,
+ 79.32, 104.55, 72.65, 84.95, 86.7, 110.43, 104.24, 68.87, 89.27, 71.86,
+ 75.66, 52.28, 62.38, 70.28, 81.25, 97.42, 96.35, 77.54, 115.46, 102.22,
+ 102.12, 90.38, 114.58, 90.14, 105.33, 125.56, 73.48, 85.2, 73.56, 52.14,
+ 79.13, 109.12, 105.15, 94.76, 69.28, 115.71, 53.15, 79.68, 72.26, 87.97,
+ 65.04, 56.63, 89.38, 59.91, 91.12, 100.2, 82.32, 61.39, 86.01, 57.14,
+ 59.39, 120.8, 95.78, 46.42, 92.45, 93.9, 58.39, 114.66, 75.91, 98.59,
+ 132.92, 57.51, 79.72, 79.21, 91.69, 79.43, 79.51, 71.48, 58.25, 45.74,
+ 113.57, 93.04, 66.41, 97, 67.86, 77.16, 70.79, 102.86, 99.47, 50.6,
+ 52.54, 93.23, 96.2, 81.92, 131.01, 78.61, 77.05, 56.92, 117.6, 79.59,
+ 92.43, 89.1, 68.27, 53.1, 66.44, 46.8, 44.67, 101.51, 81.9, 68.96,
+ 88.73, 79.31, 109.8, 87.93, 103.69, 56.66, 74.35, 52.61, 85.06, 37.7,
+ 81.6, 78.38, 62.5, 150.33, 125.78, 73.99, 50.51, 70.54, 54.44, 106,
+ 89.03, 65.17, 100.44, 61.49, 81.37, 67.39, 96.86, 51.74, 159.11, 80.12
+ ],
+ "keyDuration": [
+ 64.93, 91.3, 68.06, 69.35, 35.88, 57.56, 45.39, 54.67, 69.64, 104.58,
+ 60.56, 74.79, 69.08, 72.71, 81.32, 69.3, 70.91, 75.17, 58.29, 68.53,
+ 95.05, 81.54, 45.86, 57.07, 83.18, 66.91, 72.19, 54.46, 81.29, 84.55,
+ 45.92, 78.09, 89.63, 81.29, 77.53, 47.53, 54.43, 83.62, 88.11, 92.64,
+ 57.44, 72.23, 81.85, 73.4, 76.86, 72.37, 86.2, 82.85, 104.12, 74.07,
+ 69.73, 60.46, 74.84, 61.04, 56.71, 49.12, 65.5, 59.18, 76.71, 68.85,
+ 77.85, 91.18, 59.35, 78.48, 77.72, 56.54, 59.78, 68.7, 61.04, 78, 52.4,
+ 79.51, 49.59, 99.08, 68.46, 58.1, 69.4, 60.83, 71.33, 102.81, 68.53,
+ 63.31, 60.28, 64.71, 73.55, 80.71, 72.32, 69.87, 95.28, 73.77, 74.44,
+ 85.01, 79.06, 79.93, 84.78, 73.55, 70.67, 87.63, 73.3, 71.51, 73.86,
+ 54.4, 64.09, 34.31, 65.06, 59.97, 47.27, 71.35, 90.83, 65.16, 75.74,
+ 90.45, 60.57, 63.03, 57.74, 57.93, 70.73, 79.79, 72.15, 57.06, 61.3,
+ 52.33, 73.96, 66.1, 77.95, 84.13, 79.07, 77.57, 76.39, 86.3, 82.76,
+ 48.52, 59.08, 78.07, 79.02, 46.53, 74.2, 64.61, 103.18, 80.57, 73.32,
+ 60.12, 58.08, 86.37, 81.44, 83.29, 48.77, 56.17, 67.95, 86.69, 64.61,
+ 70.67, 75.36, 87.55, 72.88, 89.49, 82.05, 88.21, 70.78, 76.7, 86.56,
+ 57.1, 60.56, 79.37, 60.64, 77.25, 67.16, 70.47, 85.49, 53.58, 55.97,
+ 64.44, 82.94, 66.31, 77.59, 64.97, 58.88, 46.83, 55.99, 60.53, 49.26,
+ 83.65, 70.06, 77.76, 76.98, 79.95, 54.5, 83.69, 63.07, 64.08, 54.45,
+ 50.87, 75.31, 73.11, 87.06, 61.68, 56.85, 78.42, 67.39, 84.04, 57.44,
+ 80.13, 49.25, 50.67, 33.76, 78.54, 65.85, 69.58, 53.64, 87.58, 41.9,
+ 87.86, 87.78, 60.91, 62.38, 69.72, 82.88, 63.37, 58.75, 61.98, 63.93,
+ 101.04, 70.96, 66.18, 83.37, 59.78, 71.77, 49.34, 53.73, 56.99, 63.84,
+ 53.73, 42.4, 93.77, 88.46, 59.54, 56.23, 73.97, 50.48, 79.37, 74.12,
+ 64.68, 73.7, 79.66, 72.35, 54.45, 94.84, 53.5, 98.55, 88.75, 65.81,
+ 62.66, 62.15, 92.62, 67.29, 68.82, 76.89, 57.02, 77.65, 63.33, 63.93,
+ 98.14, 69.33, 52.7, 70.63, 71.33, 74.59, 47.01, 110.35, 74.35, 64.98,
+ 60.67, 61.78, 83.11, 59.77, 59.66, 69.71, 52.47, 75.79, 84.84, 67.37,
+ 63.63, 78.84, 65.11, 91.22, 58.73, 66.9, 58.53, 61.91, 58.3, 91.42,
+ 61.22, 57.28, 76.14, 99.69, 80.88, 76.45, 70.43, 77.26, 69.57, 66.96,
+ 90.1, 65.88, 84.14, 90.19, 62.89, 58.88, 64.7, 76.43, 73.63, 73.36,
+ 68.71, 75.91, 73.63, 58.24, 93.56, 76.83, 50.4, 68.87, 78.89
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/model-hand-coarse-clock.json b/packages/typing-fixtures/reference/anticheat/model-hand-coarse-clock.json
new file mode 100644
index 000000000..1387c1be3
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/model-hand-coarse-clock.json
@@ -0,0 +1,99 @@
+{
+ "name": "model-hand-coarse-clock",
+ "label": "model",
+ "client": "web",
+ "description": "Modelled hands on a 1 ms clock, as some browsers coarsen it.",
+ "samples": [
+ {
+ "keySpacing": [
+ 81, 134, 92, 96, 45, 62, 82, 722, 52, 77, 100, 75, 51, 58, 872, 69, 60,
+ 60, 80, 78, 62, 92, 79, 80, 91, 96, 129, 77, 99, 61, 104, 91, 55, 74,
+ 71, 67, 44, 78, 106, 54, 65, 77, 142, 117, 510, 143, 113, 69, 1056, 93,
+ 80, 67, 61, 85, 92, 59, 81, 64, 83, 57, 52, 110, 69, 117, 42, 47, 66,
+ 161, 87, 117, 85, 84, 68, 88, 112, 100, 113, 162, 95, 132, 54, 82, 71,
+ 115, 157, 72, 815, 83, 91, 82, 126, 44, 106, 113, 114, 86, 87, 125, 73,
+ 55, 79, 89, 54, 70, 61, 86, 101, 123, 95, 95, 84, 114, 78, 68, 107, 80,
+ 74, 65, 143, 115, 63, 74, 110, 89, 65, 934, 41, 57, 68, 85, 76, 61, 76,
+ 68, 63, 63, 97, 100, 102, 135, 89, 101, 40, 67, 1020, 103, 138, 95, 75,
+ 114, 78, 74, 81, 62, 137, 135, 108, 121, 90, 52, 107, 73, 101, 65, 51,
+ 100, 91, 81, 97, 95, 91, 116, 73, 47, 55, 75, 115, 61, 102, 104, 99,
+ 122, 66, 148, 89, 141, 75, 99, 80, 77, 82, 77, 131, 97, 60, 93, 70, 98,
+ 70, 83, 82, 88, 81, 61, 95, 85, 61, 112, 79, 94, 63, 74, 88, 87, 94, 62,
+ 46, 73, 98, 72, 150, 92, 93, 160, 95, 117, 108, 70, 82, 86, 87, 80, 123,
+ 101, 92, 95, 160, 139, 178, 125, 79, 183, 73, 79, 59, 166, 82, 62, 66,
+ 41, 59, 57, 48, 108, 96, 130, 63, 63, 102, 72, 67, 71, 66, 82, 49, 83,
+ 67, 57, 46, 144, 55, 79, 65, 73, 106, 71, 58, 96, 58, 78, 101, 91, 106,
+ 91, 101, 65, 108, 621, 119, 141, 91, 47, 62, 71, 56, 118, 63, 94, 102,
+ 130, 68, 73, 81, 520, 56, 61, 66, 774, 84, 134, 92, 102, 69, 85, 116,
+ 91, 106, 101, 72
+ ],
+ "keyDuration": [
+ 58, 97, 74, 75, 85, 47, 53, 46, 54, 66, 89, 84, 64, 63, 90, 56, 67, 140,
+ 51, 77, 82, 63, 82, 65, 87, 82, 119, 97, 81, 94, 91, 61, 56, 62, 79,
+ 104, 58, 59, 56, 65, 74, 98, 145, 88, 76, 76, 73, 83, 88, 63, 68, 93,
+ 70, 72, 70, 67, 61, 83, 53, 55, 61, 83, 79, 80, 65, 63, 92, 62, 72, 103,
+ 122, 45, 63, 72, 102, 94, 89, 85, 103, 67, 84, 72, 88, 86, 68, 64, 58,
+ 55, 64, 96, 97, 75, 94, 100, 71, 63, 81, 60, 62, 69, 61, 71, 70, 80, 51,
+ 78, 53, 84, 92, 58, 70, 61, 73, 81, 85, 104, 79, 93, 87, 48, 82, 91, 66,
+ 71, 80, 60, 81, 90, 76, 68, 66, 67, 82, 70, 57, 48, 63, 72, 62, 65, 99,
+ 64, 58, 56, 51, 85, 74, 86, 61, 90, 92, 59, 72, 98, 70, 84, 106, 98, 87,
+ 101, 87, 88, 63, 74, 83, 57, 99, 57, 50, 74, 65, 98, 80, 79, 61, 85, 83,
+ 63, 65, 65, 68, 49, 87, 107, 63, 86, 67, 75, 69, 67, 83, 75, 97, 65,
+ 109, 90, 72, 80, 86, 83, 55, 74, 78, 93, 103, 66, 75, 43, 71, 63, 48,
+ 78, 71, 59, 106, 53, 60, 94, 85, 104, 61, 45, 67, 108, 106, 126, 88, 86,
+ 54, 68, 76, 60, 73, 94, 78, 91, 108, 103, 70, 79, 46, 75, 57, 64, 109,
+ 92, 85, 61, 41, 57, 65, 62, 81, 58, 81, 50, 58, 54, 69, 82, 56, 68, 59,
+ 58, 65, 75, 50, 50, 68, 87, 49, 70, 68, 86, 83, 94, 96, 69, 73, 74, 74,
+ 108, 116, 86, 74, 96, 94, 64, 88, 55, 83, 77, 74, 76, 72, 87, 65, 73,
+ 74, 87, 46, 75, 60, 59, 50, 59, 57, 67, 58, 85, 76, 55, 78, 95, 63, 73,
+ 93, 106, 70, 89
+ ]
+ },
+ {
+ "keySpacing": [
+ 87, 86, 102, 718, 55, 76, 89, 70, 126, 167, 78, 114, 82, 215, 124, 97,
+ 106, 59, 114, 71, 697, 96, 109, 105, 109, 105, 108, 101, 179, 123, 114,
+ 84, 90, 97, 99, 120, 92, 67, 79, 88, 534, 112, 114, 90, 112, 992, 79,
+ 64, 90, 100, 72, 84, 169, 89, 88, 119, 678, 161, 150, 147, 142, 133, 70,
+ 109, 95, 88, 77, 87, 107, 75, 156, 76, 52, 104, 133, 96, 123, 64, 89,
+ 70, 94, 89, 109, 79, 154, 61, 107, 136, 98, 649, 93, 123, 114, 98, 128,
+ 134, 138, 100, 86, 78, 67, 106, 87, 89, 155, 168, 221, 151, 154, 124,
+ 100, 104, 83, 66, 92, 137, 124, 102, 102, 89, 728, 102, 101, 75, 71,
+ 135, 118, 126, 197, 587, 77, 106, 140, 81, 122, 94, 719, 118, 113, 99,
+ 80, 153, 149, 95, 88, 189, 120, 106, 111, 82, 148, 102, 119, 108, 97,
+ 75, 71, 91, 40, 716, 86, 56, 91, 127, 73, 92, 53, 93, 119, 64, 111, 99,
+ 78, 129, 71, 110, 172, 77, 105, 60, 77, 116, 97, 86, 83, 84, 84, 60, 68,
+ 114, 96, 81, 107, 136, 89, 115, 50, 90, 68, 88, 86, 92, 63, 116, 85, 97,
+ 77, 94, 104, 115, 72, 83, 133, 192, 86, 156, 70, 107, 112, 127, 102,
+ 178, 106, 168, 97, 97, 78, 115, 105, 96, 132, 84, 71, 97, 100, 197, 78,
+ 70, 102, 107, 100, 135, 84, 139, 65, 66, 50, 61, 100, 71, 48, 88, 92,
+ 78, 53, 69, 99, 131, 95, 131, 162, 137, 102, 88, 56, 57, 180, 101, 133,
+ 92, 133, 93, 87, 109, 90, 135, 127, 144, 85, 110, 99, 96, 95, 50, 1011,
+ 68, 89, 84, 63, 70, 93, 67, 155, 111, 104, 120, 76, 65, 133, 89, 79,
+ 100, 111, 74, 132, 134, 70, 117, 109, 118, 62, 88, 101, 85, 87, 80, 67,
+ 57, 80
+ ],
+ "keyDuration": [
+ 91, 80, 80, 50, 75, 92, 92, 85, 111, 100, 88, 110, 103, 127, 85, 76, 87,
+ 61, 93, 67, 70, 119, 94, 102, 125, 67, 100, 92, 105, 61, 85, 93, 87, 54,
+ 94, 87, 74, 67, 64, 90, 91, 87, 66, 82, 88, 93, 120, 75, 100, 71, 94,
+ 117, 87, 110, 108, 115, 128, 98, 107, 155, 138, 93, 86, 88, 56, 57, 109,
+ 94, 95, 88, 66, 107, 62, 78, 90, 96, 64, 63, 58, 62, 66, 70, 111, 72,
+ 93, 73, 102, 85, 71, 117, 84, 100, 114, 66, 91, 78, 77, 69, 101, 58, 63,
+ 65, 75, 159, 98, 90, 93, 79, 90, 91, 80, 131, 74, 75, 116, 76, 68, 99,
+ 70, 84, 112, 96, 91, 75, 98, 72, 80, 104, 181, 70, 127, 191, 127, 76,
+ 76, 62, 115, 157, 93, 89, 107, 118, 96, 81, 94, 94, 80, 78, 78, 78, 95,
+ 98, 79, 94, 85, 75, 134, 64, 87, 72, 86, 76, 66, 63, 62, 74, 77, 104,
+ 107, 136, 101, 77, 80, 134, 98, 73, 133, 73, 54, 67, 99, 74, 86, 95, 74,
+ 60, 99, 82, 80, 107, 122, 99, 89, 144, 102, 92, 94, 69, 82, 76, 57, 117,
+ 67, 86, 70, 98, 145, 74, 94, 77, 86, 80, 102, 98, 145, 85, 117, 101, 93,
+ 122, 95, 102, 102, 114, 115, 74, 108, 99, 62, 143, 103, 110, 102, 128,
+ 80, 72, 102, 69, 81, 73, 104, 92, 122, 75, 58, 88, 55, 94, 85, 65, 77,
+ 95, 69, 92, 59, 85, 102, 76, 76, 103, 89, 60, 122, 97, 83, 53, 69, 88,
+ 68, 97, 87, 96, 79, 69, 106, 104, 180, 78, 133, 112, 77, 69, 98, 68, 88,
+ 86, 56, 64, 63, 68, 71, 95, 74, 80, 73, 82, 72, 98, 86, 67, 93, 104, 94,
+ 53, 93, 106, 85, 66, 77, 104, 84, 63, 84, 63, 82, 95, 100, 70, 114, 119
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/model-hand.json b/packages/typing-fixtures/reference/anticheat/model-hand.json
new file mode 100644
index 000000000..b724d83f6
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/model-hand.json
@@ -0,0 +1,193 @@
+{
+ "name": "model-hand",
+ "label": "model",
+ "client": "web",
+ "description": "Three modelled hands at 70, 85 and 100 ms per key with 0.1 ms timestamps.",
+ "samples": [
+ {
+ "keySpacing": [
+ 72.7, 69.2, 46.2, 66, 50, 42.8, 107, 70.8, 41.3, 38.4, 79.2, 92.1,
+ 971.3, 34.7, 44.6, 91.4, 68.5, 62.8, 73.4, 60.4, 123.2, 80.2, 117.8,
+ 48.4, 52.8, 84.1, 88.8, 130.2, 86.3, 811.6, 83.7, 46.4, 70.3, 112.8,
+ 59.9, 39, 41.3, 64.3, 55.4, 79.2, 54.1, 61.8, 54, 54, 65.6, 40.2, 62.5,
+ 86.8, 81.3, 64, 78.6, 102.8, 189.4, 93.6, 109.2, 83, 77.8, 96.4, 121.5,
+ 123.2, 69.5, 53.8, 51.9, 66.3, 70.3, 99, 108.5, 117.7, 84.5, 83.6, 97.5,
+ 92.1, 59, 51.4, 112.7, 81.4, 1056.8, 114.1, 117.1, 728.3, 84.4, 73.2,
+ 116.3, 73, 48.5, 94.2, 58.6, 51.2, 56.8, 43, 66.9, 55.9, 101.1, 67.8,
+ 64.7, 65.9, 62.7, 55.6, 64.1, 61.8, 65.1, 54.3, 51.5, 77.6, 78.2, 86.9,
+ 74.7, 70, 119.9, 117.1, 80, 83.4, 56.9, 88.9, 73.8, 66.6, 108.4, 69.5,
+ 76.3, 82.2, 35.9, 62.1, 44.5, 483.4, 57.3, 56.1, 70.5, 57.7, 97.9, 74.8,
+ 60.9, 62.8, 64.6, 97, 67.2, 111.2, 55.9, 52.3, 83, 80, 116, 967.3, 85.4,
+ 126.2, 83.7, 49.4, 105.7, 139.9, 132.5, 94.9, 110.4, 95.2, 67.1, 735.1,
+ 97.6, 126.7, 128.7, 66.7, 48.4, 58.8, 75.1, 48.3, 56.4, 115.8, 112,
+ 92.7, 103.1, 147.6, 103.9, 85.3, 171.3, 115.1, 76.1, 60.1, 119.2, 87.9,
+ 70, 100, 77.4, 56.9, 84, 126, 66.2, 98, 55.3, 52.1, 624.7, 53.6, 105.7,
+ 67.4, 63, 95.2, 83.6, 62.7, 62.5, 58.3, 66.1, 92.4, 68.7, 104.6, 51.5,
+ 47.8, 69.1, 72.6, 95.8, 81.4, 72.1, 158.3, 114.9, 83, 55.6, 57.9, 65.9,
+ 37.8, 74.4, 87.6, 61.6, 789.6, 65.6, 72.1, 87, 47.7, 83.2, 71.3, 111.5,
+ 73.5, 64, 51.7, 80.8, 77.7, 64, 87.8, 88.5, 62.6, 78, 72.2, 117.8, 79.1,
+ 103.3, 76.2, 57.5, 82.1, 87.4, 107, 79.9, 92.7, 90.7, 77.7, 95.6, 112.8,
+ 60.8, 53.6, 85.6, 65.7, 46.7, 67.5, 65.4, 37.7, 70.8, 71.8, 111.2, 84.4,
+ 109.8, 122.5, 60, 64.2, 68, 50.8, 49.6, 96.4, 68.9, 53.1, 78.5, 80.7,
+ 59.3, 113, 58, 53.1, 54.9, 48.5, 88.4, 44.7, 49.6, 53, 43.4, 73.8, 73,
+ 57.6, 68.9, 51.1, 67.8, 56.3, 103.8, 50.6, 57.9, 53, 73.8, 60.5, 82.1,
+ 94.2, 83.8, 75.1, 51, 39.3, 50.8, 70.3, 41.1, 64.3, 61.7, 95.1, 86.9,
+ 50.1, 73.1, 63.5, 60.3, 68.7, 86.5, 48.1, 47.7
+ ],
+ "keyDuration": [
+ 69.9, 48.1, 45.8, 52.7, 48.8, 55.4, 48.7, 66.6, 70.2, 61.2, 49.5, 51.4,
+ 79, 61.8, 42.1, 72.3, 60.7, 79.8, 67.8, 29.6, 65.4, 49.6, 58.8, 79.5,
+ 47.3, 70.2, 67.6, 72.5, 80.2, 74.3, 69.2, 73, 69.4, 56.7, 77.4, 53.6,
+ 82.7, 47.6, 62.6, 50.5, 46.7, 54.6, 72.1, 54.1, 55.2, 54.1, 88, 107.2,
+ 59.6, 73.1, 67.9, 97.6, 51, 98.8, 104.4, 73.3, 83.2, 76, 73.7, 55.5,
+ 56.8, 68.6, 41.5, 82.6, 69.5, 69.5, 72.3, 72.1, 78.1, 59.2, 59.6, 57.6,
+ 78.1, 69.9, 64.7, 126.7, 68.7, 58, 88.4, 60.3, 58.5, 83.4, 67.4, 47.1,
+ 41.8, 60.9, 75.6, 50.5, 66.3, 70.9, 80.8, 76.2, 56.2, 73.3, 57.2, 101.2,
+ 63, 69.6, 45.7, 46.5, 70.8, 74.6, 50.9, 56.6, 55.9, 75.2, 59.5, 69.1,
+ 99.9, 101.4, 92.4, 83.6, 102.5, 110, 71.9, 72.1, 56.9, 103.6, 99.6,
+ 45.4, 55.8, 62.6, 58.7, 59, 64, 60.3, 57.9, 52.8, 81.5, 58.3, 53.8,
+ 53.2, 70.9, 86.9, 52, 68.3, 88.4, 44.5, 64.9, 71.1, 71.3, 54.4, 53.1,
+ 86.6, 93.8, 74.8, 74.5, 95.8, 113.4, 75.6, 78.3, 63.9, 75.7, 46.8, 60.2,
+ 76.4, 91.8, 60.3, 59.6, 50.4, 56.5, 57.2, 72.1, 86.5, 72.3, 88.1, 95.2,
+ 70.1, 39.7, 58.6, 97.4, 101.1, 65.2, 76, 72.5, 67, 92, 74.6, 91.5, 83.9,
+ 86.7, 74, 70.6, 80.3, 94.1, 59.3, 53.5, 98.5, 61, 46.2, 44.1, 40.2,
+ 49.5, 38, 73.7, 75.6, 62.5, 92.1, 56.6, 63.1, 52.2, 64.4, 67.1, 69.3,
+ 56.5, 62.5, 72.1, 76.5, 88.9, 68, 61.1, 39.8, 61.2, 71.7, 74.3, 68.1,
+ 51.4, 55.6, 81, 75.1, 50.2, 48.6, 56.3, 55.2, 42.2, 51.6, 84.4, 56.2,
+ 88.1, 57.4, 56.9, 73.6, 97.2, 99.2, 72.3, 59.4, 74.9, 77, 83.6, 68,
+ 55.8, 128.3, 82.4, 99.3, 78.7, 62, 66.6, 69.8, 68.5, 75.2, 62.7, 76.3,
+ 55.2, 49.8, 50.4, 62.9, 52.6, 77.3, 76.6, 85.6, 65.9, 74.1, 73.1, 55.7,
+ 53, 78.9, 64, 77.7, 63.2, 62, 62.9, 74.7, 59.1, 71.1, 74.3, 89.1, 58.2,
+ 66.7, 54.3, 36.2, 60, 38.2, 53.4, 38.5, 61.5, 50.8, 70.6, 54.9, 80,
+ 70.5, 53.4, 42.5, 36.6, 45.2, 71.7, 51.5, 67.9, 57.9, 67.6, 62.5, 61.9,
+ 46.8, 44.4, 43.5, 66.3, 41.1, 38.4, 76.7, 49.8, 54.2, 59.6, 86.4, 51.8,
+ 57.9, 79.1, 43.1, 48.1, 45.7, 52.2, 53.2
+ ]
+ },
+ {
+ "keySpacing": [
+ 102.8, 157.6, 98.1, 107.1, 53.6, 74.6, 59.4, 61.5, 179.9, 37.9, 69.5,
+ 76, 75.3, 50.5, 63, 95.6, 62.7, 53.4, 79.2, 161.7, 123.4, 126, 67.4,
+ 118.9, 98.1, 71.3, 576.7, 62.5, 61.5, 70.3, 114.6, 102, 56.9, 82.5,
+ 84.2, 70.5, 64.3, 99.5, 68.5, 76.8, 156.9, 142.6, 62.2, 75.5, 62.1,
+ 125.9, 47.5, 66.1, 71.4, 45.3, 95.3, 125.2, 48.3, 105.8, 85.3, 70,
+ 144.7, 86, 71, 91, 85.5, 547.9, 80.8, 121.8, 63, 106.4, 119, 88.4, 60.3,
+ 62.5, 790.6, 78.8, 84.7, 62.9, 114.4, 87.3, 109.7, 95.3, 77.6, 75.1, 92,
+ 69.1, 68.8, 88.4, 95.7, 111, 105.4, 87.5, 75.8, 87.3, 88.4, 45.3, 83.6,
+ 52.5, 82.2, 91.6, 146.2, 106.5, 63.7, 86.5, 66.2, 46, 594.5, 50.2, 54.1,
+ 74, 54, 53.5, 66.9, 66.8, 64.6, 145.4, 76.9, 108.9, 90.7, 119.4, 80.8,
+ 80.2, 109.4, 114.2, 127.2, 116.1, 66.5, 54.9, 96, 53.9, 76.1, 168.6,
+ 74.2, 65.8, 117.5, 51.9, 69.7, 74.9, 81.2, 58.1, 73.5, 62.1, 73.5, 58.9,
+ 61.6, 44.9, 86.5, 86.6, 107, 74, 79.3, 47.1, 99.5, 78.2, 105.9, 85.5,
+ 94.9, 68.2, 53.6, 81, 68.7, 102.9, 51.4, 99.7, 64.4, 49.6, 66.4, 44.9,
+ 69.5, 83.7, 111.5, 67.1, 125.1, 112.6, 89.9, 77.8, 139.7, 118.1, 130.6,
+ 108.5, 95.1, 924.3, 90.6, 94.9, 117.2, 124.3, 115.2, 119.2, 107.7, 85.4,
+ 112.9, 66.8, 90.5, 78.4, 90.4, 56.8, 51.1, 110.3, 79.8, 65, 66.6, 78.5,
+ 43.9, 84.3, 113.4, 68.5, 64.1, 65.2, 75.2, 43.6, 82.1, 53.5, 39.5, 82.9,
+ 113.8, 167.1, 112.7, 113.8, 179, 131.3, 70.5, 92.5, 906.8, 143.3, 47.5,
+ 97.2, 78.7, 123.7, 100.8, 58.2, 73.8, 72.8, 92.1, 75.5, 68.3, 156.1,
+ 36.9, 90.9, 40.6, 59.6, 72.7, 114.9, 63.5, 59.3, 99.9, 123.1, 158.3,
+ 108.9, 97.1, 99.6, 95.8, 87.3, 101.9, 106.5, 92.3, 77.9, 101.4, 95.9,
+ 107.7, 131.9, 90.4, 130.4, 133.9, 132.7, 88.4, 78.7, 103.5, 141.5,
+ 110.4, 86.2, 105.4, 92.8, 124.6, 110.9, 51.5, 66.3, 126.5, 98.2, 44.2,
+ 47.6, 54.6, 88.9, 68.5, 79.5, 72.3, 62.6, 49.1, 66.6, 53.2, 99.5, 60.4,
+ 74.4, 76.7, 82.5, 104.3, 66.1, 76.3, 91.5, 104.4, 95.4, 83.1, 60.7,
+ 73.4, 86.5, 81.1, 78.2, 66.1, 98.6, 69.5, 98.3, 81.8, 121.2, 565, 56.5,
+ 54.5, 94.1, 98.5, 114.6, 104.7, 108, 93.5, 124.4, 56.5
+ ],
+ "keyDuration": [
+ 70.4, 57.9, 66.9, 94.4, 68.4, 99.6, 76.2, 63.4, 57.3, 80.2, 68.5, 71.1,
+ 103, 81, 67.5, 64.6, 75.1, 93.5, 79.3, 102.6, 55, 59.8, 84.5, 72.1,
+ 59.8, 75.5, 53.5, 60.4, 72.1, 94.6, 93.1, 61.2, 109.2, 63.5, 70.8, 60.9,
+ 69.6, 95.1, 91.3, 70.1, 77.5, 99.9, 83.8, 115.6, 64.6, 64.1, 60, 70.5,
+ 45.9, 42.7, 60.6, 86.6, 80.3, 60.7, 123.3, 75.3, 94.5, 73.6, 69.1, 99.8,
+ 87, 67.5, 82.7, 86, 73.1, 72.1, 85.8, 73.2, 111.4, 53.4, 61.9, 79.5,
+ 63.6, 98.5, 86.3, 65.7, 80.9, 51.8, 99.4, 91.7, 75.4, 62.3, 66, 63,
+ 80.9, 65.7, 58.1, 89.8, 71.2, 62.4, 78.7, 52.3, 80.3, 91.6, 71.7, 73.7,
+ 85, 84.7, 65, 70.7, 62.4, 38.1, 57.2, 54.7, 44.3, 50.6, 50.9, 71.2, 63,
+ 74.1, 92.1, 105.3, 82.2, 69.8, 98.1, 61.4, 58.7, 116.8, 147.1, 118.9,
+ 59.5, 82, 50.7, 79.3, 95.9, 74.7, 105.9, 114.3, 76.3, 72.8, 87.9, 97.7,
+ 78.3, 63.9, 60.3, 59.7, 44.9, 52.4, 69.2, 62.2, 74.2, 48.1, 65.1, 97.4,
+ 78.3, 71, 63.5, 46.9, 73.9, 60.9, 68.4, 63.7, 71.8, 67.3, 47.9, 71.2,
+ 69.8, 86.3, 42.9, 75.3, 43.7, 69.8, 93.4, 70.8, 59.9, 47.4, 47.2, 111,
+ 59.8, 82.4, 118.1, 81.2, 72.9, 101.7, 78.9, 80.7, 96.5, 105.8, 77.3,
+ 74.3, 85.3, 120.8, 151.3, 96.5, 119.9, 103, 109.6, 97.8, 135, 79.9,
+ 67.7, 66.6, 76.2, 57.6, 55.9, 62.2, 48.3, 58.7, 78.3, 72.7, 95.3, 69.8,
+ 75.7, 65.2, 114.4, 85.2, 109.1, 83.9, 63.1, 82.4, 77, 108.4, 61.2, 88.5,
+ 88.9, 70.4, 89.3, 140.6, 63.1, 67.8, 97.1, 81.2, 62, 82.8, 89.6, 87,
+ 109.3, 52.6, 61.7, 66.3, 83.6, 86.6, 71.4, 61.2, 107.5, 70.8, 96.2,
+ 74.9, 90.4, 51.1, 80.1, 81.4, 99.6, 86.7, 87.6, 99.6, 60.6, 49.5, 77.5,
+ 72.4, 95.1, 104.5, 92.8, 106.8, 84, 95, 139.8, 159.5, 52, 91.6, 83.7,
+ 72.2, 77.8, 116.7, 97.9, 80.8, 91.4, 89.1, 84.1, 79, 99.8, 66.7, 52.6,
+ 89.3, 42.8, 73.8, 64.1, 46.1, 66.9, 56.9, 59.3, 65.4, 65.4, 63.1, 51.9,
+ 44.5, 78.9, 75.1, 65.6, 79.7, 57.4, 62.9, 67, 56, 56.7, 75.2, 72.6,
+ 89.3, 94.3, 89.7, 88.9, 95.1, 66.4, 84.6, 68.1, 81.6, 59.8, 61.3, 65.2,
+ 53.1, 41.4, 93, 121.4, 77.5, 73.1, 86.3, 71.2, 102.3, 60.1, 80.9
+ ]
+ },
+ {
+ "keySpacing": [
+ 170.1, 83.9, 95.5, 68.1, 98.4, 50.2, 159, 157, 105.8, 75, 88.4, 82.9,
+ 187.3, 150.2, 112.3, 141.5, 139.6, 994.1, 111.4, 123.8, 94.5, 119.5,
+ 101.2, 93.7, 114.8, 82.4, 121, 131.5, 137.7, 129.3, 132.2, 196.4, 144.9,
+ 202, 194.7, 129.9, 163.7, 101.5, 620, 124.7, 173.5, 111.5, 186.5, 72.1,
+ 96.6, 95.2, 72.7, 53.4, 74.2, 144.5, 71.1, 826.5, 58.2, 89.5, 93, 99,
+ 61.8, 133.3, 109.1, 138.9, 63.2, 77.3, 133.4, 70, 119.5, 98.4, 76.8,
+ 146.4, 75.5, 152.1, 86.4, 133.2, 92.7, 96.4, 130.6, 138.6, 121.7, 91.8,
+ 64.6, 82.6, 61.5, 112, 171.7, 132.5, 82.9, 95.9, 78.4, 142.2, 84.9,
+ 67.8, 83.8, 122.2, 139.7, 128.9, 97.3, 106, 84.4, 155.3, 135.1, 81.5,
+ 86.1, 870.6, 97.5, 83.4, 103.6, 127.1, 123.8, 98.9, 77.4, 103.3, 150.6,
+ 87.7, 116.6, 784.8, 104.5, 79.7, 130.6, 141.2, 95.2, 207.8, 98.7, 111.6,
+ 182.9, 112.2, 91.4, 96, 655.7, 157.5, 103.9, 100.1, 84.1, 105.7, 50.4,
+ 137.8, 141.8, 174.4, 128.8, 144.5, 127.3, 81, 71.3, 103.3, 101, 92.5,
+ 90.2, 118.3, 728.2, 154.8, 167.7, 181.3, 104.2, 88.7, 59, 75.9, 77.7,
+ 132.2, 103.2, 84.8, 50.3, 101.4, 99.8, 108.8, 45.4, 84.4, 89.7, 56,
+ 113.8, 105.6, 44.2, 113.2, 135.5, 94.7, 106.8, 75.9, 156.5, 80.7, 96.9,
+ 103.8, 97.4, 96, 109.2, 71.4, 154.1, 89.3, 76.1, 109.6, 92.8, 210.8,
+ 150, 55, 102.2, 136.2, 115.3, 75.2, 82.3, 90.6, 46.8, 60.3, 64.2, 120.5,
+ 117.2, 122.9, 57.2, 72.9, 78.7, 117.4, 113.1, 67.8, 113.4, 159, 134.7,
+ 96.5, 1009.1, 159.9, 102.5, 125.3, 72.6, 79.2, 102.9, 125.6, 169.1,
+ 74.3, 847.2, 184.2, 89.7, 154.4, 121.2, 113.3, 107.5, 508.8, 82.8,
+ 122.6, 90.9, 80.7, 122.7, 88.6, 72.8, 937, 148.7, 105.5, 98, 116.7,
+ 80.7, 104.9, 802.3, 118.7, 106.3, 74.2, 67.3, 132.4, 97.5, 93.8, 111,
+ 118, 92.3, 142.7, 104.3, 88.5, 112.8, 85, 85, 78.8, 76.5, 102.5, 139.9,
+ 146.8, 105.2, 118.8, 112.8, 73.7, 114.3, 95.1, 101.2, 128, 122.6, 88.7,
+ 84.6, 74.2, 108.4, 92.7, 148.6, 73.4, 89.5, 111.5, 99.2, 132.4, 116.5,
+ 105.8, 120.7, 70.6, 90.6, 78.7, 160.6, 121, 100, 63.9, 97.5, 104.2,
+ 99.8, 86.4, 49.9, 92.9, 68.1, 62.1, 91.7, 85.8, 79.4, 59.6, 107.6,
+ 119.4, 98.9, 162.3, 177.5, 71.5, 103.8, 121.9, 86.1, 105.4, 130.2
+ ],
+ "keyDuration": [
+ 145, 90.6, 63.5, 63.1, 96.8, 106.4, 119.3, 99.3, 107.5, 103.8, 76.7, 75,
+ 94.1, 91.5, 118.1, 100.1, 106.1, 89.1, 103.8, 82.4, 95.2, 68.9, 95.5,
+ 88.1, 115.6, 81.7, 114.5, 90.7, 139.7, 134.4, 94, 132.9, 86, 130.8,
+ 131.9, 107.1, 96.5, 187.5, 102, 94.2, 78.4, 131.4, 84, 95.2, 124.3,
+ 92.1, 79.3, 85.9, 61.9, 50.8, 59.4, 114.5, 58.3, 66.4, 55.2, 66.9, 72.9,
+ 79.6, 118.7, 123.8, 102.4, 103.2, 111.8, 104.2, 115.6, 71.8, 92.5, 64,
+ 80.2, 95.4, 85.1, 115.2, 97.5, 123, 124.7, 112.3, 63.6, 94.5, 77.5,
+ 80.7, 77.4, 147, 94.1, 98.6, 80.6, 69.4, 103.9, 108.3, 90.1, 98.4, 87.9,
+ 94.4, 111.5, 95, 86.6, 89.3, 84.1, 99.4, 140.7, 82.1, 95.9, 94.8, 123.5,
+ 109.6, 102.9, 119.3, 165.5, 112.2, 75.3, 101.1, 68.1, 66, 114.1, 73.7,
+ 79.5, 83.2, 82.8, 115.9, 94.4, 109.9, 113.3, 106.5, 119.5, 92.5, 79.6,
+ 84, 62.8, 86.7, 81.7, 79.7, 93.8, 104.9, 76.5, 97.2, 88.6, 106.4, 73.2,
+ 150.5, 96.3, 91.2, 83.8, 86.4, 89.9, 73.2, 68.6, 73.6, 114.3, 86.5,
+ 75.5, 94.5, 92.8, 77, 105.1, 77.2, 88.2, 74.7, 94.9, 120.9, 109, 73.1,
+ 72.9, 85.7, 48.5, 67.7, 72.9, 61.5, 84.1, 83.6, 71.6, 85.9, 81.3, 99,
+ 79.8, 72.3, 87, 105.1, 103.8, 75.6, 118.1, 81.4, 113.9, 93.7, 86.5,
+ 110.6, 72.7, 121, 74.5, 126.4, 104.1, 111.4, 100.2, 101, 75, 82.8,
+ 101.3, 109.8, 68, 47.6, 62.1, 69.2, 99.7, 58, 83.3, 89.9, 105.6, 68.7,
+ 137.1, 83.6, 122.9, 136.3, 97.5, 106.2, 123.9, 87.1, 86.9, 91.4, 73.6,
+ 107.9, 78.1, 118.9, 120.8, 100.9, 113.3, 80.5, 139.1, 128.9, 83.9,
+ 143.2, 85.2, 65.4, 89.2, 127.8, 100.8, 92.7, 95.7, 78.8, 69.8, 111.9,
+ 116.1, 68.4, 123, 78.2, 105.8, 82.9, 74.3, 64.4, 102.8, 91.8, 84.8, 90,
+ 75.3, 120.3, 164.3, 123.1, 76.6, 135.8, 72.9, 88.2, 79.8, 109.1, 75.9,
+ 85.4, 115.4, 131.7, 121.4, 111.8, 101.4, 106.2, 99.8, 94.9, 95.9, 121.5,
+ 64.9, 72.6, 117.8, 113.2, 72.3, 70.7, 93.1, 100.7, 99, 102.8, 123, 77.2,
+ 67, 110.4, 70.2, 75.1, 109.8, 84.5, 68.9, 72.7, 64.8, 109.2, 116.7,
+ 70.8, 82.7, 75.8, 72.2, 43.3, 81.8, 80.9, 94, 56, 57.2, 65, 89.9, 79.7,
+ 82, 61.7, 106.4, 110.4, 108.3, 73.4, 94.4, 76.6, 136.6, 109.7, 85.2,
+ 79.9
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/ms-jitter.json b/packages/typing-fixtures/reference/anticheat/ms-jitter.json
new file mode 100644
index 000000000..0a8ea46c8
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/ms-jitter.json
@@ -0,0 +1,78 @@
+{
+ "name": "ms-jitter",
+ "label": "bot",
+ "client": "web",
+ "description": "80/45 ms with ±1 ms uniform jitter, escaping the exact check.",
+ "samples": [
+ {
+ "keySpacing": [
+ 80.85, 79.67, 79.44, 79.16, 79.47, 80.02, 79.15, 80.95, 80.24, 80.38,
+ 80.54, 79.74, 80.46, 80.75, 80.06, 80.49, 79.84, 79.98, 80.47, 80.86,
+ 80.96, 79.94, 79.7, 80.58, 79.54, 80.5, 80.06, 79.69, 79.91, 79.02,
+ 79.97, 80.08, 80.53, 79.19, 80.61, 79.81, 79.66, 80.09, 79.12, 80.25,
+ 79.56, 80.11, 79.29, 80.91, 80.71, 80.84, 79.7, 79.37, 80.32, 80, 79.14,
+ 80.53, 80.91, 79.01, 79.96, 80.81, 80.38, 80.11, 80.97, 79.58, 80.53,
+ 79.72, 79.93, 80.4, 79.01, 79.48, 80.01, 80.58, 80.71, 79.14, 79.92,
+ 80.28, 80.96, 80.23, 79.56, 80.42, 80.54, 79.62, 80.91, 80.55, 79.36,
+ 80.97, 79.77, 80.96, 80.87, 79.92, 79.24, 79.16, 79.02, 79.23, 80.34,
+ 79.23, 80.8, 80.87, 80.36, 79.7, 79.77, 80.34, 80.82, 79.5, 80.26,
+ 80.46, 80.21, 80.73, 80.87, 80.42, 79.22, 80.33, 80.69, 79.3, 79.56,
+ 80.47, 79.75, 81, 80.83, 79.06, 80.53, 79.81, 80.8, 80.02, 79.1, 80.38,
+ 79.09, 80.8, 80.22, 80.13, 79.88, 80.56, 79.77, 79.57, 80.89, 81, 79.67,
+ 79.17, 80.51, 79.15, 80.65, 79.63, 79.91, 80.24, 79.28, 80.73, 79.86,
+ 80.78, 80.55, 80.59, 80.8, 79.69, 79.94, 80.1, 80.18, 79.86, 79.71,
+ 79.95, 80.16, 80.23, 80.5, 79.28, 80.5, 80.86, 80.18, 79.39, 79.73,
+ 80.38, 79.78, 79.67, 80.08, 80.77, 79.95, 80.29, 79.8, 80.65, 79.38,
+ 79.73, 80.76, 79.12, 79.17, 79.27, 79.32, 80.05, 80.29, 79.79, 80.43,
+ 79.32, 80.5, 80.26, 79.04, 79.23, 80.18, 80.88, 80.17, 79.08, 79.32,
+ 80.86, 80.35, 80.78, 80.75, 80.78, 80.25, 80.98, 80.48, 80.56, 80.95,
+ 79.1, 80.78, 80.98, 80.03, 80.54, 80.23, 80.76, 79.34, 80.43, 80.56,
+ 80.36, 80.62, 79.39, 79.37, 80.14, 79.45, 79.69, 80.69, 80.69, 80.9,
+ 79.83, 79.81, 79.28, 80.87, 80.3, 80.02, 80.74, 80.62, 80.39, 80.08,
+ 80.62, 80.49, 79.05, 80.62, 80.42, 79.63, 79.9, 79.67, 79.59, 79.55,
+ 79.99, 79.79, 80.57, 80.87, 79.49, 80.45, 79.94, 80.97, 79.23, 79.13,
+ 80.88, 80.95, 79.21, 80.52, 79.44, 80.51, 79.54, 79.7, 79.5, 79.69,
+ 80.42, 80.7, 79.23, 79.95, 80.85, 80.9, 80.3, 80.97, 80.19, 80.03, 80.3,
+ 80.75, 80.38, 80.95, 80.37, 79.97, 79.02, 80.92, 79.29, 80.63, 79.8,
+ 79.64, 79.63, 79.84, 79.12, 79.1, 80.6, 79.73, 80.87, 79.36, 80.07,
+ 79.89, 80.14, 80.39, 81, 80.47, 79.15, 80.99, 80.03, 80.71, 79.65,
+ 80.83, 79.98, 80.17, 80.93, 80.5, 80.09, 79.7, 79.79, 80.31, 79.52,
+ 80.25, 80.99, 79.05, 80.95, 79.27
+ ],
+ "keyDuration": [
+ 44.92, 45.79, 45.81, 44.48, 44.01, 44.88, 45.7, 44.04, 45.18, 45.72,
+ 45.32, 44.12, 45.07, 44.23, 45.97, 44.59, 44.46, 44.56, 44.55, 44.26,
+ 44.78, 44.01, 44.7, 45.11, 44.99, 44.08, 45.27, 45.68, 45.58, 45.66,
+ 45.65, 44.13, 45.78, 45.47, 45.22, 45.54, 44.34, 45.02, 45.86, 45.58,
+ 44.28, 45.37, 44.71, 45.05, 45.51, 44.72, 45.3, 44.19, 44.03, 44.77,
+ 45.42, 44.21, 45.17, 45.95, 45.6, 44.16, 45.46, 45.34, 44.36, 45.8,
+ 45.78, 44.77, 44.15, 45.89, 44.91, 45.95, 44.49, 45.31, 44.71, 45.35,
+ 45.48, 45.42, 45.9, 45.21, 45.62, 44.38, 44.83, 45.66, 45.63, 44.68,
+ 45.6, 45.78, 44.41, 45.93, 44.97, 45.12, 44.52, 44.19, 44.21, 45.97,
+ 45.54, 44.79, 45.14, 44.96, 44.31, 45.48, 44.27, 45.31, 44.52, 45.56,
+ 44.47, 45.71, 45.1, 45.5, 44.15, 45.51, 44.33, 45.64, 45.46, 45.34,
+ 44.46, 44.93, 45.69, 44.15, 45.47, 44.09, 45.17, 45.31, 45.73, 45.49,
+ 45.9, 44.08, 44.37, 44.4, 44.01, 44.54, 45.26, 44.43, 45.46, 45.89,
+ 45.62, 44.66, 45.23, 44.93, 44.55, 45.19, 44.17, 44.21, 44.78, 44.53,
+ 45.5, 44.45, 44.23, 44.03, 45.57, 44.16, 45.52, 44.25, 44.94, 45.74,
+ 44.47, 44.92, 44.69, 45.06, 45.89, 45.69, 44.45, 44.6, 44.61, 45.51,
+ 44.42, 45.23, 44.54, 44.79, 44.61, 45.79, 44.2, 45.4, 44.58, 45.12,
+ 45.27, 44.37, 44.88, 44.34, 45.15, 44.71, 44.06, 45.83, 44.25, 44.17,
+ 44.8, 45.71, 44.62, 44.44, 45.97, 45.33, 44.89, 45.88, 45.66, 45.5,
+ 45.49, 45.79, 44.35, 45.26, 45.5, 45.18, 44.65, 44.67, 45.91, 44.29,
+ 45.49, 45.33, 44.26, 45.45, 44.32, 45.33, 45.36, 44.11, 45.61, 44.97,
+ 45.26, 44.36, 44.08, 45.86, 45.99, 44.9, 44.78, 45.81, 45.16, 45, 44.56,
+ 45.05, 44.16, 45.41, 44.19, 45.99, 44.65, 45.25, 45.22, 44.15, 44.14,
+ 44.33, 44.15, 44.16, 45.91, 45, 44.79, 44.22, 44.74, 44.91, 45.72,
+ 45.89, 45.76, 44.3, 45.94, 44.42, 44.52, 44.28, 44.86, 45.83, 45.79,
+ 44.1, 45.11, 45.61, 44.27, 44.08, 45.39, 44.79, 45.49, 45.38, 44.44,
+ 45.47, 44.97, 44.89, 44.7, 44.31, 44.22, 45.51, 45.09, 45.74, 44.18,
+ 45.56, 45.09, 45.53, 45.73, 44.6, 45.93, 44.46, 45.45, 45.7, 45.27,
+ 44.13, 44.35, 44.84, 44.58, 45.58, 44.94, 44.14, 44.78, 45.1, 45.32,
+ 45.17, 44.48, 44.24, 45.39, 45.44, 45.45, 45.92, 45.08, 45.65, 45.91,
+ 44.63, 44.93, 44.56, 45.03, 45.45, 45.06, 45.53, 45.42, 44.48, 44.83,
+ 44.36, 44.89, 45.84, 45.74, 45.66, 44.24, 45, 45.57, 45.52
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/replayed-recording.json b/packages/typing-fixtures/reference/anticheat/replayed-recording.json
new file mode 100644
index 000000000..b65b6b7aa
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/replayed-recording.json
@@ -0,0 +1,205 @@
+{
+ "name": "replayed-recording",
+ "label": "bot",
+ "client": "web",
+ "description": "One modelled timeline uploaded by the same user, resubmitted exactly and then with ±0.3 ms noise.",
+ "samples": [
+ {
+ "keySpacing": [
+ 94.8, 85, 100.3, 57.8, 75.1, 100.1, 89.4, 72.6, 72, 45.7, 63.7, 52.6,
+ 117.5, 59.6, 84.6, 92.7, 161.6, 95.4, 110, 121.5, 69.9, 185.8, 71.5,
+ 58.3, 97.6, 55, 101.9, 90.8, 127.7, 85.4, 109.6, 73.9, 146.4, 67.9,
+ 107.7, 76, 73.6, 96.9, 124.9, 134, 76.2, 36.9, 77.1, 105.7, 70.5, 72.2,
+ 73.5, 76.8, 67.6, 60.8, 76.3, 53.1, 134.4, 79.3, 111.5, 130.1, 114.9,
+ 111.7, 81, 143.9, 133.2, 106.8, 103.8, 90.2, 74.3, 125.6, 832.5, 48.6,
+ 77, 77.1, 72.9, 89.8, 82.1, 87.8, 140, 128.4, 40.5, 50.5, 51, 67.5,
+ 133.4, 71.6, 92.1, 81.4, 113, 86.9, 84.4, 85.1, 72.1, 84.3, 106.7,
+ 124.1, 84.4, 105.4, 51.7, 57.4, 84.7, 61.5, 52.9, 86.8, 51.8, 66.3, 102,
+ 56.9, 80.8, 94.4, 67.1, 63.5, 121.9, 83.3, 60.2, 51.7, 103.2, 75.6,
+ 96.8, 79.4, 93.9, 91.9, 119.3, 178.3, 74.8, 72.1, 89.8, 80.3, 38.4,
+ 144.7, 98.2, 96.9, 61, 120.3, 133.8, 99.1, 77.5, 118.1, 84.5, 86.6,
+ 77.1, 68.1, 76.3, 107.9, 54.3, 106.5, 96.6, 174.8, 89.3, 105.7, 111,
+ 82.9, 120.5, 124.9, 82.4, 119.3, 178.1, 81.4, 115.5, 105.8, 90.7, 69.9,
+ 67.3, 85.8, 78, 120.7, 97.8, 51.3, 89.6, 87.7, 100, 106.6, 77.8, 93,
+ 80.6, 94.1, 109.1, 70, 91.5, 95.1, 88.5, 62.4, 63.7, 43, 65, 58.2,
+ 119.6, 617.5, 48.6, 56.8, 75, 112.3, 65.5, 95.2, 57.5, 92.5, 64.9, 68.6,
+ 66.7, 58.1, 76.2, 65.3, 89.4, 75.8, 88.7, 76.5, 71.2, 63.1, 82.9, 47.9,
+ 77.6, 88.4, 102.3, 90.4, 54.8, 624.4, 62, 68.2, 73.5, 86.8, 171, 93.9,
+ 125.5, 104.8, 76.7, 66, 84.5, 107.2, 155.9, 105, 125.4, 134.6, 92.7,
+ 92.2, 84.5, 109.4, 90.7, 92.2, 54.4, 93.3, 105.1, 78.4, 61.4, 56.7,
+ 62.6, 743.8, 56.3, 53.7, 76.2, 102.6, 87.7, 82.9, 75.6, 95.9, 215.3,
+ 81.3, 82.8, 85.7, 78.3, 71.3, 101.3, 97, 89.8, 78.9, 76.6, 120.4, 117.6,
+ 56.6, 96.9, 99.2, 65.1, 66.6, 90.4, 69.7, 70, 63.8, 75.6, 78.5, 81.3,
+ 931.8, 75.3, 169.4, 105.8, 150.4, 91, 103.8, 173.5, 104.2, 102.5, 88.8,
+ 85.2, 65.7, 67.6, 88.2, 109.5, 91.5, 69.4, 126.3, 102.2, 78.4, 63,
+ 107.1, 101.4, 140.4, 104.5, 106.3, 62.8, 82.4, 87.1, 107.2, 94.3, 98,
+ 82.1, 105.7, 121.4, 92.1, 81.5, 87.4, 153.9, 91.2, 126.1, 112.5, 68
+ ],
+ "keyDuration": [
+ 89.7, 107.6, 79.8, 111.5, 83.8, 73.6, 82.2, 70.5, 70.2, 47.1, 88.4,
+ 74.1, 45.7, 61.7, 133.1, 90.7, 75.3, 114.1, 76.1, 89.7, 108.4, 96.4,
+ 70.6, 89.8, 105.4, 80.5, 71.8, 69.3, 90.5, 60.6, 104.3, 90.4, 86.4,
+ 97.7, 69.5, 79.1, 85.6, 69.5, 110.5, 128.7, 54.1, 112.7, 122.3, 51.3,
+ 59.7, 67.8, 62.7, 75.2, 65.4, 64.3, 76, 70.9, 72.8, 106.4, 97.4, 100.3,
+ 69.2, 86.1, 90.8, 89.7, 72.4, 101.3, 65.2, 93.5, 55.5, 80.7, 91.5, 61.2,
+ 75.6, 53.1, 67.5, 70.6, 79.2, 88.2, 56.5, 83.5, 77.3, 68, 60.6, 70.5,
+ 46.1, 64.8, 85.8, 80.4, 54.7, 64.7, 73, 76.8, 81.7, 77, 84.1, 87.6,
+ 64.6, 67.6, 46.8, 73, 63.1, 64.7, 49.9, 49.7, 73.7, 85.6, 63.7, 84.9,
+ 70.5, 53.6, 57.9, 40.9, 55.9, 59, 73.5, 74.6, 71.3, 58.5, 67.3, 78.7,
+ 85.6, 113.7, 98.5, 92.1, 75, 110, 119.7, 67, 63.4, 99.3, 87.6, 80.9,
+ 77.4, 85, 94.3, 57.2, 76.3, 65, 65.2, 67, 78.1, 69.8, 77.2, 112.3,
+ 104.2, 64.7, 94, 81.7, 113.2, 101.1, 124.5, 76.5, 85.1, 100.2, 68.5, 83,
+ 104.2, 61.2, 91.6, 114, 103.8, 51.9, 77.2, 76.6, 54.7, 76.9, 85.4, 66.3,
+ 56.6, 62, 74.5, 62.1, 73.2, 73.3, 63.9, 96.2, 105.6, 84.3, 116.3, 86.3,
+ 79.9, 94.3, 73.3, 80.1, 69, 56, 56.3, 82.3, 58.9, 59.4, 65.3, 54.2,
+ 70.9, 72.5, 70.4, 97.5, 65.1, 71, 92.6, 65.8, 71, 69.1, 44.3, 69.2,
+ 63.7, 77, 118.4, 61.1, 58.8, 70.7, 91.9, 51.1, 51.9, 111.3, 54.7, 83.4,
+ 55, 59.2, 57.2, 67.5, 72.4, 52.5, 105.6, 86.8, 89.3, 91.5, 101, 74.1,
+ 91.3, 102.4, 86.6, 93.6, 102.6, 103.4, 94.2, 61.3, 66, 71.7, 54.9, 60.1,
+ 76.9, 65.9, 54.2, 90.6, 80, 40.3, 87.8, 59.2, 62, 76, 60.1, 83.4, 68.2,
+ 66.7, 91.1, 111.8, 69.9, 80.7, 102.6, 67.6, 62.4, 81.9, 83.3, 54.7,
+ 68.7, 89.5, 90.5, 77, 79.1, 83.7, 49.5, 56.1, 57.3, 62.5, 40.1, 62.6,
+ 72.7, 85.4, 89.7, 68.9, 92.4, 93.8, 112.1, 54.5, 111.8, 75.7, 98.3,
+ 67.5, 42.8, 57.4, 65.7, 53.6, 82.1, 70.2, 125.3, 84.6, 91.9, 64.2, 77.5,
+ 55.4, 89.3, 85.6, 92, 59.4, 56.3, 84, 87.7, 90.6, 95.1, 78.3, 107.2,
+ 50.3, 64.4, 97.9, 91, 70.6, 112.9, 86.7, 101.2, 127.4, 71, 73.1, 68.1,
+ 89.4
+ ],
+ "label": "model"
+ },
+ {
+ "keySpacing": [
+ 94.8, 85, 100.3, 57.8, 75.1, 100.1, 89.4, 72.6, 72, 45.7, 63.7, 52.6,
+ 117.5, 59.6, 84.6, 92.7, 161.6, 95.4, 110, 121.5, 69.9, 185.8, 71.5,
+ 58.3, 97.6, 55, 101.9, 90.8, 127.7, 85.4, 109.6, 73.9, 146.4, 67.9,
+ 107.7, 76, 73.6, 96.9, 124.9, 134, 76.2, 36.9, 77.1, 105.7, 70.5, 72.2,
+ 73.5, 76.8, 67.6, 60.8, 76.3, 53.1, 134.4, 79.3, 111.5, 130.1, 114.9,
+ 111.7, 81, 143.9, 133.2, 106.8, 103.8, 90.2, 74.3, 125.6, 832.5, 48.6,
+ 77, 77.1, 72.9, 89.8, 82.1, 87.8, 140, 128.4, 40.5, 50.5, 51, 67.5,
+ 133.4, 71.6, 92.1, 81.4, 113, 86.9, 84.4, 85.1, 72.1, 84.3, 106.7,
+ 124.1, 84.4, 105.4, 51.7, 57.4, 84.7, 61.5, 52.9, 86.8, 51.8, 66.3, 102,
+ 56.9, 80.8, 94.4, 67.1, 63.5, 121.9, 83.3, 60.2, 51.7, 103.2, 75.6,
+ 96.8, 79.4, 93.9, 91.9, 119.3, 178.3, 74.8, 72.1, 89.8, 80.3, 38.4,
+ 144.7, 98.2, 96.9, 61, 120.3, 133.8, 99.1, 77.5, 118.1, 84.5, 86.6,
+ 77.1, 68.1, 76.3, 107.9, 54.3, 106.5, 96.6, 174.8, 89.3, 105.7, 111,
+ 82.9, 120.5, 124.9, 82.4, 119.3, 178.1, 81.4, 115.5, 105.8, 90.7, 69.9,
+ 67.3, 85.8, 78, 120.7, 97.8, 51.3, 89.6, 87.7, 100, 106.6, 77.8, 93,
+ 80.6, 94.1, 109.1, 70, 91.5, 95.1, 88.5, 62.4, 63.7, 43, 65, 58.2,
+ 119.6, 617.5, 48.6, 56.8, 75, 112.3, 65.5, 95.2, 57.5, 92.5, 64.9, 68.6,
+ 66.7, 58.1, 76.2, 65.3, 89.4, 75.8, 88.7, 76.5, 71.2, 63.1, 82.9, 47.9,
+ 77.6, 88.4, 102.3, 90.4, 54.8, 624.4, 62, 68.2, 73.5, 86.8, 171, 93.9,
+ 125.5, 104.8, 76.7, 66, 84.5, 107.2, 155.9, 105, 125.4, 134.6, 92.7,
+ 92.2, 84.5, 109.4, 90.7, 92.2, 54.4, 93.3, 105.1, 78.4, 61.4, 56.7,
+ 62.6, 743.8, 56.3, 53.7, 76.2, 102.6, 87.7, 82.9, 75.6, 95.9, 215.3,
+ 81.3, 82.8, 85.7, 78.3, 71.3, 101.3, 97, 89.8, 78.9, 76.6, 120.4, 117.6,
+ 56.6, 96.9, 99.2, 65.1, 66.6, 90.4, 69.7, 70, 63.8, 75.6, 78.5, 81.3,
+ 931.8, 75.3, 169.4, 105.8, 150.4, 91, 103.8, 173.5, 104.2, 102.5, 88.8,
+ 85.2, 65.7, 67.6, 88.2, 109.5, 91.5, 69.4, 126.3, 102.2, 78.4, 63,
+ 107.1, 101.4, 140.4, 104.5, 106.3, 62.8, 82.4, 87.1, 107.2, 94.3, 98,
+ 82.1, 105.7, 121.4, 92.1, 81.5, 87.4, 153.9, 91.2, 126.1, 112.5, 68
+ ],
+ "keyDuration": [
+ 89.7, 107.6, 79.8, 111.5, 83.8, 73.6, 82.2, 70.5, 70.2, 47.1, 88.4,
+ 74.1, 45.7, 61.7, 133.1, 90.7, 75.3, 114.1, 76.1, 89.7, 108.4, 96.4,
+ 70.6, 89.8, 105.4, 80.5, 71.8, 69.3, 90.5, 60.6, 104.3, 90.4, 86.4,
+ 97.7, 69.5, 79.1, 85.6, 69.5, 110.5, 128.7, 54.1, 112.7, 122.3, 51.3,
+ 59.7, 67.8, 62.7, 75.2, 65.4, 64.3, 76, 70.9, 72.8, 106.4, 97.4, 100.3,
+ 69.2, 86.1, 90.8, 89.7, 72.4, 101.3, 65.2, 93.5, 55.5, 80.7, 91.5, 61.2,
+ 75.6, 53.1, 67.5, 70.6, 79.2, 88.2, 56.5, 83.5, 77.3, 68, 60.6, 70.5,
+ 46.1, 64.8, 85.8, 80.4, 54.7, 64.7, 73, 76.8, 81.7, 77, 84.1, 87.6,
+ 64.6, 67.6, 46.8, 73, 63.1, 64.7, 49.9, 49.7, 73.7, 85.6, 63.7, 84.9,
+ 70.5, 53.6, 57.9, 40.9, 55.9, 59, 73.5, 74.6, 71.3, 58.5, 67.3, 78.7,
+ 85.6, 113.7, 98.5, 92.1, 75, 110, 119.7, 67, 63.4, 99.3, 87.6, 80.9,
+ 77.4, 85, 94.3, 57.2, 76.3, 65, 65.2, 67, 78.1, 69.8, 77.2, 112.3,
+ 104.2, 64.7, 94, 81.7, 113.2, 101.1, 124.5, 76.5, 85.1, 100.2, 68.5, 83,
+ 104.2, 61.2, 91.6, 114, 103.8, 51.9, 77.2, 76.6, 54.7, 76.9, 85.4, 66.3,
+ 56.6, 62, 74.5, 62.1, 73.2, 73.3, 63.9, 96.2, 105.6, 84.3, 116.3, 86.3,
+ 79.9, 94.3, 73.3, 80.1, 69, 56, 56.3, 82.3, 58.9, 59.4, 65.3, 54.2,
+ 70.9, 72.5, 70.4, 97.5, 65.1, 71, 92.6, 65.8, 71, 69.1, 44.3, 69.2,
+ 63.7, 77, 118.4, 61.1, 58.8, 70.7, 91.9, 51.1, 51.9, 111.3, 54.7, 83.4,
+ 55, 59.2, 57.2, 67.5, 72.4, 52.5, 105.6, 86.8, 89.3, 91.5, 101, 74.1,
+ 91.3, 102.4, 86.6, 93.6, 102.6, 103.4, 94.2, 61.3, 66, 71.7, 54.9, 60.1,
+ 76.9, 65.9, 54.2, 90.6, 80, 40.3, 87.8, 59.2, 62, 76, 60.1, 83.4, 68.2,
+ 66.7, 91.1, 111.8, 69.9, 80.7, 102.6, 67.6, 62.4, 81.9, 83.3, 54.7,
+ 68.7, 89.5, 90.5, 77, 79.1, 83.7, 49.5, 56.1, 57.3, 62.5, 40.1, 62.6,
+ 72.7, 85.4, 89.7, 68.9, 92.4, 93.8, 112.1, 54.5, 111.8, 75.7, 98.3,
+ 67.5, 42.8, 57.4, 65.7, 53.6, 82.1, 70.2, 125.3, 84.6, 91.9, 64.2, 77.5,
+ 55.4, 89.3, 85.6, 92, 59.4, 56.3, 84, 87.7, 90.6, 95.1, 78.3, 107.2,
+ 50.3, 64.4, 97.9, 91, 70.6, 112.9, 86.7, 101.2, 127.4, 71, 73.1, 68.1,
+ 89.4
+ ]
+ },
+ {
+ "keySpacing": [
+ 94.8, 85.25, 100.41, 57.98, 75.07, 100.07, 89.22, 72.35, 71.72, 46,
+ 63.95, 52.38, 117.44, 59.53, 84.57, 92.96, 161.47, 95.58, 110.26,
+ 121.29, 69.83, 185.92, 71.51, 58.29, 97.48, 54.91, 101.83, 90.81,
+ 127.43, 85.41, 109.32, 73.64, 146.22, 67.69, 107.45, 75.79, 73.76,
+ 96.89, 124.85, 134.08, 76.38, 36.69, 77.07, 105.95, 70.34, 72.37, 73.61,
+ 76.58, 67.66, 61.03, 76.36, 52.81, 134.55, 79.43, 111.57, 130.28,
+ 114.93, 111.63, 80.89, 144.05, 132.97, 106.73, 103.63, 89.93, 74.53,
+ 125.56, 832.47, 48.39, 77.23, 77.05, 73.12, 90.06, 81.84, 87.6, 139.88,
+ 128.69, 40.77, 50.53, 50.7, 67.32, 133.29, 71.74, 92.33, 81.61, 113.21,
+ 86.8, 84.22, 84.98, 72.39, 84.54, 106.81, 124.26, 84.4, 105.35, 51.54,
+ 57.26, 84.77, 61.41, 53.07, 86.52, 51.83, 66.53, 102.18, 56.9, 80.75,
+ 94.59, 67.34, 63.49, 121.73, 83.16, 59.93, 51.94, 103.09, 75.71, 96.9,
+ 79.43, 93.77, 91.81, 119.18, 178.35, 74.78, 72.21, 89.67, 80.44, 38.67,
+ 144.76, 97.98, 96.62, 60.81, 120.48, 133.79, 99.21, 77.61, 118.28,
+ 84.22, 86.61, 76.97, 68.1, 76.04, 107.75, 54.12, 106.74, 96.75, 175.01,
+ 89.24, 105.53, 110.92, 83.08, 120.76, 124.72, 82.25, 119.3, 177.98,
+ 81.54, 115.73, 105.6, 90.62, 69.7, 67.51, 86.06, 78.15, 120.9, 97.74,
+ 51.06, 89.8, 87.92, 100.03, 106.49, 77.61, 93.01, 80.42, 94.1, 108.99,
+ 70.02, 91.54, 94.83, 88.23, 62.42, 63.68, 42.99, 64.93, 58.18, 119.8,
+ 617.32, 48.63, 57.02, 74.94, 112.56, 65.55, 94.93, 57.31, 92.41, 64.96,
+ 68.68, 66.88, 57.91, 76.32, 65.57, 89.61, 75.69, 88.91, 76.42, 70.99,
+ 63.16, 83.09, 48.08, 77.58, 88.26, 102.42, 90.39, 54.87, 624.35, 61.76,
+ 68.34, 73.59, 86.96, 171.23, 94.18, 125.66, 105.06, 76.81, 65.98, 84.44,
+ 107.34, 155.78, 104.77, 125.23, 134.56, 92.46, 92.3, 84.36, 109.16,
+ 90.78, 92.18, 54.48, 93.6, 104.85, 78.4, 61.18, 56.79, 62.6, 743.85,
+ 56.4, 53.48, 76.14, 102.65, 87.94, 82.81, 75.51, 96.11, 215.12, 81.55,
+ 82.85, 85.48, 78.56, 71.15, 101.12, 96.9, 89.64, 79.13, 76.48, 120.17,
+ 117.33, 56.88, 97.03, 99.16, 65.09, 66.77, 90.61, 69.55, 70.06, 63.95,
+ 75.74, 78.66, 81.16, 931.7, 75.06, 169.52, 106.03, 150.69, 91.14, 103.8,
+ 173.55, 104.08, 102.64, 89.03, 85.24, 65.69, 67.85, 88.12, 109.67,
+ 91.49, 69.4, 126.43, 102.1, 78.37, 63.12, 107.08, 101.11, 140.15,
+ 104.72, 106.35, 62.62, 82.64, 87.08, 107.29, 94.01, 97.8, 82.33, 105.97,
+ 121.13, 92.26, 81.61, 87.63, 153.8, 90.91, 125.89, 112.2, 67.82
+ ],
+ "keyDuration": [
+ 89.6, 107.77, 79.77, 111.61, 83.5, 73.63, 82.21, 70.2, 70.07, 47.26,
+ 88.12, 74.04, 45.93, 61.62, 132.87, 90.45, 75.21, 114.07, 75.84, 89.56,
+ 108.46, 96.44, 70.8, 89.72, 105.66, 80.5, 71.75, 69.49, 90.51, 60.65,
+ 104.39, 90.68, 86.67, 97.45, 69.66, 78.99, 85.49, 69.22, 110.29, 128.54,
+ 54.29, 112.94, 122.1, 51.6, 59.71, 67.51, 62.63, 75.34, 65.63, 64.16,
+ 75.72, 70.84, 72.86, 106.41, 97.18, 100.15, 69.05, 85.89, 90.96, 89.72,
+ 72.11, 101.54, 65.16, 93.44, 55.33, 80.96, 91.75, 61.36, 75.76, 53.24,
+ 67.47, 70.88, 79.18, 87.94, 56.75, 83.51, 77.52, 68.07, 60.69, 70.7,
+ 45.96, 64.86, 85.87, 80.6, 54.42, 64.71, 73.06, 77.05, 81.99, 76.75,
+ 83.96, 87.48, 64.6, 67.71, 46.85, 72.94, 63.18, 64.95, 49.81, 49.7,
+ 73.57, 85.71, 63.6, 84.96, 70.45, 53.52, 58.04, 40.99, 55.86, 58.88,
+ 73.44, 74.78, 71.46, 58.42, 67.14, 78.64, 85.73, 113.91, 98.22, 91.98,
+ 75.13, 109.95, 119.98, 67.2, 63.45, 99.46, 87.73, 81.02, 77.52, 85.22,
+ 94.08, 57.01, 76.47, 64.74, 65.49, 67.13, 78.33, 70.06, 77.18, 112.54,
+ 104.23, 64.85, 94.1, 81.66, 113.15, 101.06, 124.46, 76.55, 84.96,
+ 100.05, 68.24, 83.01, 104.15, 61.14, 91.57, 113.83, 103.69, 51.94,
+ 77.23, 76.54, 54.98, 76.77, 85.48, 66.39, 56.33, 61.99, 74.34, 62.1,
+ 72.99, 73.53, 64.1, 96.28, 105.57, 84.36, 116.24, 86.11, 79.62, 94.39,
+ 73.24, 80.13, 69.13, 55.8, 56, 82.47, 58.84, 59.58, 65.28, 54.35, 70.69,
+ 72.39, 70.2, 97.64, 65.32, 70.97, 92.63, 65.65, 70.81, 69.4, 44.04, 69,
+ 63.94, 77.18, 118.33, 61.22, 58.72, 70.64, 91.83, 50.88, 51.98, 111.2,
+ 54.57, 83.45, 54.85, 59.47, 57.48, 67.33, 72.19, 52.3, 105.41, 86.54,
+ 89.53, 91.38, 101.18, 73.85, 91.06, 102.29, 86.89, 93.43, 102.75,
+ 103.57, 94.14, 61.32, 66.25, 71.59, 54.94, 60.2, 76.75, 66.18, 54.48,
+ 90.57, 79.79, 40.38, 87.6, 59.22, 62.24, 75.93, 60.03, 83.21, 68.39,
+ 66.97, 91.24, 111.96, 70.04, 80.68, 102.66, 67.8, 62.57, 81.78, 83.11,
+ 54.67, 68.77, 89.8, 90.29, 77.29, 79.37, 83.96, 49.55, 56.38, 57.42,
+ 62.33, 39.9, 62.73, 72.92, 85.62, 89.64, 68.97, 92.68, 94.07, 111.96,
+ 54.33, 111.99, 75.58, 98.05, 67.21, 42.96, 57.17, 65.72, 53.35, 82.37,
+ 69.96, 125.43, 84.61, 91.62, 64.45, 77.61, 55.26, 89.04, 85.42, 92.04,
+ 59.14, 56.48, 84.22, 88, 90.67, 95.26, 78.37, 107.29, 50.24, 64.34,
+ 97.75, 90.81, 70.55, 113.16, 86.82, 101.38, 127.54, 71.23, 73.1, 68.17,
+ 89.17
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/scripted-playwright.json b/packages/typing-fixtures/reference/anticheat/scripted-playwright.json
new file mode 100644
index 000000000..267978ff5
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/scripted-playwright.json
@@ -0,0 +1,55 @@
+{
+ "name": "scripted-playwright",
+ "label": "bot",
+ "client": "web",
+ "description": "The fast scripted presses record-fixtures.ts sends: uniform 60-120 ms gaps and 40-150 ms holds.",
+ "samples": [
+ {
+ "keySpacing": [
+ 100, 93, 60, 118, 116, 61, 87, 98, 84, 119, 76, 111, 102, 118, 73, 72,
+ 73, 84, 95, 94, 103, 76, 109, 85, 99, 80, 75, 76, 65, 65, 90, 89, 63,
+ 100, 77, 67, 95, 104, 70, 85, 69, 110, 66, 78, 90, 106, 76, 80, 70, 71,
+ 69, 104, 97, 114, 109, 116, 102, 83, 73, 66, 65, 107, 90, 118, 92, 105,
+ 76, 92, 111, 75, 64, 103, 117, 115, 90, 79, 84, 81, 95, 88, 117, 62, 84,
+ 86, 114, 61, 106, 98, 77, 81, 86, 84, 107, 114, 94, 83, 97, 67, 95, 109,
+ 97, 65, 115, 107, 81, 101, 79, 101, 75, 92, 107, 116, 73, 116, 96, 65,
+ 107, 116, 80, 119, 101, 77, 97, 78, 93, 63, 107, 113, 67, 98, 100, 100,
+ 102, 84, 94, 113, 84, 81, 80, 120, 68, 68, 119, 105, 79, 62, 110, 84,
+ 119, 64, 89, 67, 108, 119, 77, 105, 112, 100, 82, 113, 110, 100, 63,
+ 116, 89, 81, 87, 80, 87, 108, 102, 89, 116, 89, 71, 109, 106, 99, 120,
+ 110, 117, 71, 75, 101, 65, 100, 119, 73, 75, 79, 72, 86, 83, 64, 109,
+ 77, 116, 102, 62, 67, 63, 61, 114, 103, 95, 119, 91, 108, 77, 61, 76,
+ 106, 117, 98, 79, 111, 105, 91, 65, 81, 68, 106, 65, 93, 64, 62, 104,
+ 68, 78, 120, 120, 69, 89, 86, 87, 108, 79, 64, 63, 110, 110, 88, 108,
+ 111, 104, 71, 70, 120, 87, 70, 76, 74, 101, 104, 68, 116, 98, 85, 106,
+ 117, 97, 81, 72, 114, 94, 107, 114, 71, 101, 73, 105, 76, 102, 88, 115,
+ 112, 73, 77, 105, 85, 85, 102, 82, 79, 82, 66, 94, 103, 97, 103, 119,
+ 77, 101, 95, 96, 93, 63, 113, 103, 74, 103, 95, 66, 104, 103, 65, 92,
+ 118, 96, 111, 60, 111, 91, 67, 82, 93, 105, 115, 100
+ ],
+ "keyDuration": [
+ 40, 62, 48, 141, 63, 82, 46, 126, 93, 62, 144, 144, 148, 144, 144, 101,
+ 72, 131, 104, 137, 136, 103, 134, 62, 80, 42, 72, 73, 81, 80, 142, 53,
+ 92, 89, 72, 142, 58, 116, 139, 105, 120, 61, 49, 104, 121, 137, 61, 98,
+ 99, 53, 85, 122, 41, 46, 86, 131, 83, 42, 138, 116, 82, 61, 105, 70,
+ 142, 106, 72, 83, 119, 52, 135, 54, 98, 49, 77, 94, 66, 71, 96, 142,
+ 149, 125, 97, 110, 54, 43, 87, 59, 147, 88, 71, 146, 98, 70, 41, 44,
+ 139, 141, 87, 79, 148, 97, 83, 124, 60, 137, 65, 62, 101, 139, 81, 61,
+ 145, 115, 117, 96, 49, 72, 90, 146, 109, 42, 69, 83, 130, 60, 52, 65,
+ 60, 107, 109, 134, 135, 83, 51, 86, 69, 90, 144, 143, 115, 60, 51, 106,
+ 54, 149, 90, 75, 106, 133, 101, 40, 108, 68, 146, 146, 136, 111, 44, 42,
+ 96, 137, 55, 71, 112, 90, 127, 129, 67, 93, 80, 129, 83, 80, 45, 148,
+ 133, 130, 55, 45, 146, 69, 66, 131, 99, 142, 49, 79, 88, 49, 54, 58, 83,
+ 76, 65, 40, 104, 92, 126, 144, 136, 150, 75, 136, 111, 97, 136, 105,
+ 138, 57, 52, 118, 140, 119, 134, 81, 72, 56, 122, 139, 104, 46, 72, 55,
+ 53, 76, 127, 139, 88, 68, 46, 94, 126, 146, 132, 96, 93, 75, 127, 84,
+ 84, 98, 42, 55, 73, 146, 115, 105, 100, 67, 124, 148, 127, 86, 53, 120,
+ 55, 115, 86, 65, 128, 138, 85, 122, 125, 65, 113, 127, 61, 113, 127, 82,
+ 100, 109, 65, 60, 137, 138, 49, 137, 67, 60, 127, 54, 94, 106, 90, 145,
+ 101, 107, 92, 144, 47, 42, 53, 100, 143, 96, 59, 67, 145, 79, 108, 79,
+ 139, 105, 57, 130, 60, 103, 137, 90, 105, 102, 145, 90, 110, 138, 59,
+ 107
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/sub-ms-jitter.json b/packages/typing-fixtures/reference/anticheat/sub-ms-jitter.json
new file mode 100644
index 000000000..379510463
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/sub-ms-jitter.json
@@ -0,0 +1,82 @@
+{
+ "name": "sub-ms-jitter",
+ "label": "bot",
+ "client": "web",
+ "description": "80/45 ms with jitter inside the 0.01 ms fixed-timing tolerance.",
+ "samples": [
+ {
+ "keySpacing": [
+ 80.002, 79.996, 80, 79.997, 80.002, 80, 80, 79.997, 79.998, 80, 79.998,
+ 79.997, 80.002, 80, 80.002, 80.003, 79.999, 80.002, 79.998, 80.001, 80,
+ 80.003, 80, 79.996, 80.003, 79.998, 80.003, 80.002, 79.999, 80.002,
+ 79.997, 79.999, 80.002, 79.996, 79.996, 80, 80.003, 80.001, 80.001,
+ 80.002, 80.001, 80.001, 79.997, 79.999, 80, 80.003, 79.999, 80, 80,
+ 80.001, 80.002, 80.003, 80.001, 79.999, 80.002, 80.002, 80, 80.004,
+ 80.003, 80.002, 80.001, 80.004, 80.002, 79.996, 79.997, 80, 80.003,
+ 79.999, 80, 79.998, 80, 80, 80.001, 79.999, 80.002, 79.997, 80.004,
+ 80.001, 80.002, 80.002, 79.996, 79.999, 79.997, 80.002, 80.002, 80.001,
+ 80.004, 80.004, 80.004, 80.001, 80.003, 80.004, 80.003, 79.999, 80.001,
+ 80.004, 79.999, 80.001, 79.997, 80.001, 80, 79.998, 80.003, 79.996,
+ 80.003, 79.997, 79.999, 80.003, 79.996, 80.001, 79.997, 79.998, 80.004,
+ 80, 79.997, 79.996, 79.997, 80.001, 80.001, 80.003, 80.002, 80.002,
+ 79.997, 79.998, 79.997, 80.001, 80.001, 80.001, 80.002, 79.999, 80.004,
+ 79.997, 80.004, 79.998, 79.999, 80, 79.999, 80, 79.998, 80.001, 79.996,
+ 80, 80.002, 79.999, 80, 79.999, 80.004, 79.997, 79.997, 79.997, 79.997,
+ 80.003, 80.002, 80.003, 79.999, 79.997, 79.999, 79.998, 79.997, 79.997,
+ 79.999, 79.998, 79.996, 80, 80, 79.999, 80.004, 80.003, 80.003, 79.998,
+ 79.999, 79.998, 79.998, 80.002, 79.996, 79.998, 79.999, 80.004, 79.998,
+ 80.001, 79.997, 80.001, 80.003, 79.996, 80.004, 79.999, 79.997, 79.996,
+ 80, 80.001, 80.003, 80, 80.001, 80.003, 80.001, 80.003, 80.003, 80.001,
+ 79.998, 80.001, 80.004, 80, 79.998, 79.997, 80, 79.998, 80.001, 80.003,
+ 79.997, 80.002, 80.001, 79.999, 79.998, 80.003, 80.002, 80, 80.003,
+ 79.999, 80.003, 80, 79.999, 80.002, 79.999, 80.001, 79.998, 80.002, 80,
+ 80.001, 80.003, 80.003, 80.003, 79.999, 79.998, 79.996, 79.998, 80.001,
+ 80.001, 80.004, 79.998, 80.002, 79.998, 80.003, 79.998, 80, 80.001,
+ 80.003, 80.002, 79.997, 80.003, 79.998, 80.003, 80.003, 80.002, 80.002,
+ 79.999, 79.997, 79.998, 80.001, 80, 80, 80.001, 79.997, 80.003, 80.002,
+ 80.001, 80.003, 80.003, 80.004, 80.004, 80.003, 79.996, 79.999, 80,
+ 79.997, 80.001, 80.001, 80.003, 80.002, 79.998, 80, 80, 79.997, 79.997,
+ 79.999, 80.002, 79.999, 79.999, 80.001, 80, 79.997, 79.997, 79.999,
+ 79.996, 79.999, 79.997, 79.999, 79.999, 80.002, 79.999, 80.002, 80,
+ 80.004, 80.004, 80.001, 80.002, 80.002, 80.002, 80.002, 80, 80.004,
+ 79.998, 79.999, 79.999, 79.998, 80.002, 80.002, 79.999, 80, 79.999
+ ],
+ "keyDuration": [
+ 44.999, 44.996, 44.998, 45, 45.002, 45.002, 45.003, 45, 44.997, 45.002,
+ 44.998, 45, 45, 45.001, 45, 45.001, 44.998, 45.003, 44.999, 44.999,
+ 45.002, 44.997, 44.998, 45.003, 44.998, 45.003, 44.999, 45.001, 44.996,
+ 44.997, 44.997, 45, 45.001, 44.996, 44.998, 45.001, 45.002, 44.999,
+ 44.998, 44.998, 44.997, 44.996, 44.999, 45.004, 44.998, 44.996, 44.997,
+ 44.998, 45.001, 45, 44.997, 45, 44.997, 44.996, 44.997, 44.996, 44.999,
+ 45.002, 44.997, 45.001, 44.997, 45.002, 44.997, 44.997, 45.003, 44.999,
+ 44.999, 45.003, 45.003, 45.002, 44.999, 45.002, 45.002, 44.998, 44.998,
+ 45.004, 45.001, 45.003, 45.002, 44.999, 45.001, 44.999, 44.998, 44.998,
+ 45.003, 45.001, 44.996, 44.997, 45, 45.002, 45.002, 44.999, 45.002,
+ 44.998, 45.002, 44.999, 44.996, 45.002, 44.997, 45, 45.003, 45, 45, 45,
+ 45.004, 44.999, 44.997, 44.999, 44.997, 44.996, 45.001, 44.997, 45.001,
+ 44.997, 45.001, 44.999, 45.001, 45, 44.999, 45, 45.002, 45.003, 44.996,
+ 44.997, 45.002, 45, 44.999, 45, 45, 45, 45.003, 44.997, 45.003, 45,
+ 44.997, 45.003, 45.004, 45.002, 45, 45, 44.998, 44.999, 44.997, 44.997,
+ 45.001, 45.001, 45.003, 45.003, 44.997, 45.002, 45, 44.999, 44.997, 45,
+ 45.003, 45, 45, 44.997, 45.003, 45.002, 45.003, 44.999, 44.999, 45.001,
+ 45.003, 44.997, 45.001, 45.001, 44.997, 44.998, 45.002, 45, 44.998,
+ 45.004, 44.998, 45.003, 44.997, 45.003, 44.997, 45, 44.998, 44.996,
+ 44.997, 44.999, 44.997, 44.998, 45.002, 44.999, 44.999, 44.999, 45.003,
+ 44.997, 45.004, 44.999, 45.002, 45, 45.002, 45.003, 45.001, 45.003,
+ 44.998, 45.004, 45.002, 45.001, 45.001, 45.002, 44.996, 45.002, 45.001,
+ 45.002, 44.996, 44.999, 44.999, 45.002, 45.001, 44.997, 45.001, 45,
+ 44.999, 44.999, 44.996, 45.004, 44.998, 44.999, 45.002, 44.997, 44.999,
+ 44.997, 45, 44.998, 45, 45.002, 44.997, 45.002, 45.004, 44.999, 45.003,
+ 45.004, 44.997, 44.999, 45.002, 45.002, 45.004, 44.999, 45.004, 44.998,
+ 44.997, 45, 45.003, 45.002, 45.001, 45.001, 44.999, 45, 45.002, 45.001,
+ 44.997, 45.001, 45.001, 44.999, 44.997, 44.998, 44.999, 44.998, 45.003,
+ 44.996, 44.997, 45.004, 45, 45.003, 45.003, 45.001, 44.998, 45.001,
+ 44.998, 45.001, 45, 45.002, 45.003, 45, 44.997, 45.001, 45.003, 45.002,
+ 44.996, 45.003, 44.997, 44.998, 44.996, 44.999, 45.002, 45, 45, 45.002,
+ 45.002, 44.999, 45.001, 44.998, 45.001, 45, 45, 45.002, 44.997, 45.002,
+ 45, 44.999, 44.996, 45.002, 44.997, 45.003, 44.999, 44.997, 44.999,
+ 44.997, 45.003, 44.998, 45.004, 44.999, 44.998, 45.001
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/uniform-range.json b/packages/typing-fixtures/reference/anticheat/uniform-range.json
new file mode 100644
index 000000000..9e2ceaf83
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/uniform-range.json
@@ -0,0 +1,78 @@
+{
+ "name": "uniform-range",
+ "label": "bot",
+ "client": "web",
+ "description": "Independent uniform gaps 60-100 ms and holds 20-40 ms.",
+ "samples": [
+ {
+ "keySpacing": [
+ 87.59, 90.91, 68.79, 84.93, 63.41, 83.69, 88.8, 78.32, 96.22, 69.18,
+ 91.19, 89.62, 86.42, 60.55, 78.62, 72.02, 95.86, 71.52, 90.21, 88.94,
+ 70.14, 60.76, 91.74, 68.33, 70.9, 64.93, 78.8, 99.67, 69.82, 63.24,
+ 93.94, 72.68, 76.3, 99.43, 70.88, 75.44, 68.69, 94.77, 88.89, 95.15,
+ 80.46, 92.2, 85.97, 65.59, 85.44, 83.1, 72.61, 91.16, 84.38, 60.33,
+ 97.28, 73.66, 74, 94.98, 69.97, 67.01, 93.35, 71.16, 99.93, 60.55,
+ 74.04, 90.34, 72.57, 86.43, 63.07, 63.2, 89.4, 80.99, 70.14, 97.2,
+ 61.98, 89.39, 98.15, 71.41, 81.33, 94.26, 93.87, 83.16, 98.26, 88.1,
+ 81.61, 64.4, 71.76, 81.18, 66.2, 70.08, 90.67, 65.54, 63.2, 73.07,
+ 92.22, 85.73, 76.5, 63.9, 71.62, 98.28, 73.06, 88.11, 89.77, 64.09,
+ 91.34, 97.27, 84.23, 64.46, 74.47, 76.12, 62.07, 76.85, 96.71, 72.45,
+ 90.67, 62.08, 92.66, 64.03, 80.48, 93.07, 68.39, 83.86, 64.04, 74.55,
+ 86.54, 70.17, 87.25, 69.59, 70.13, 99.39, 85.73, 78.12, 89.69, 61.55,
+ 84.74, 86.23, 60.77, 65.79, 73.35, 73.4, 85.7, 97.95, 83.74, 94.74,
+ 90.94, 71.34, 62.55, 66.8, 61.66, 69.08, 95.64, 78.08, 61.69, 91.09,
+ 83.45, 71.44, 95.77, 90.98, 98.81, 67.32, 70.9, 92.2, 70.97, 71.75,
+ 90.63, 60.16, 86.33, 81.21, 70.82, 69.73, 86.61, 92.38, 82.57, 84.21,
+ 86.85, 77.05, 82.76, 67.97, 64.26, 74.87, 79.84, 62.52, 84.36, 78.16,
+ 78.61, 75.36, 80.02, 74.29, 70.09, 77.4, 81.05, 86.45, 74.35, 69.81,
+ 91.6, 62.7, 74.66, 87.59, 65.19, 65.46, 99.69, 63.23, 80.68, 75.67,
+ 65.33, 68.35, 99.33, 75.71, 61.23, 84.53, 87.5, 90.73, 80.73, 69.06,
+ 67.53, 94.63, 98.36, 80.32, 64.25, 87.19, 79.78, 72.39, 64.31, 79.47,
+ 99.43, 80.75, 67.27, 69.27, 98.2, 68.13, 91.54, 89.03, 83.76, 60.21,
+ 77.29, 96.85, 60.05, 90.62, 79.85, 82.26, 77.7, 72.74, 86.27, 94.41,
+ 70.78, 74.48, 60.15, 71.23, 92.59, 79.85, 94.12, 99.11, 80.18, 85.89,
+ 63.23, 64.36, 80.11, 80.37, 81.3, 85.32, 76.7, 60.18, 82.43, 99.38,
+ 64.25, 66.08, 74.41, 94.84, 97.36, 95.16, 78.54, 60.85, 90.46, 73.31,
+ 94.92, 87.79, 62.36, 83.63, 94.37, 98.58, 77.88, 62.67, 87.73, 82.96,
+ 91.58, 68.29, 83.88, 74.85, 84.54, 82.11, 75.22, 66.21, 75.65, 75.57,
+ 67.94, 72.97, 75.41, 90.96, 78.55, 68.89, 69.5, 87.68, 71.63, 92.14,
+ 78.25, 64.77, 83.23, 95.73, 65.7, 88.96, 82.33, 89.29, 80.37, 75.64,
+ 94.22, 79.84, 68.11, 63.81, 60.2, 71.58, 92.73, 76.85, 86.2
+ ],
+ "keyDuration": [
+ 20.64, 36.45, 22.7, 31.83, 30.92, 28.04, 24.77, 25.95, 34.28, 27.62,
+ 32.56, 26.69, 26.55, 24.88, 27.99, 37.73, 35.15, 28.83, 32.52, 37.4,
+ 25.14, 27.54, 31.55, 20.34, 31.42, 20.07, 20.97, 37, 38.65, 27.63,
+ 29.98, 37.42, 24.71, 24.29, 32.49, 25.38, 34.25, 27.15, 21.18, 39.35,
+ 33.07, 35.76, 35.62, 30.39, 30.22, 27.32, 26.78, 35.46, 38.25, 39.2,
+ 23.19, 35.39, 21.22, 22.17, 36.63, 39.36, 33.54, 30.26, 29.65, 27.74,
+ 30.81, 39.31, 21.27, 22.99, 32.12, 37.69, 23.82, 33.52, 37.7, 20.91,
+ 35.96, 35.44, 36.83, 28.83, 25.47, 22.95, 29.51, 25.86, 29.9, 34.96,
+ 24.18, 36.56, 23.59, 36.91, 36.58, 28.8, 24.88, 23.69, 20.37, 36.39,
+ 38.14, 21.25, 38.9, 27.01, 28, 26.97, 29.45, 28.23, 20.67, 20.28, 20.9,
+ 25.8, 28.47, 24.22, 28.37, 30.65, 38.99, 27.58, 37.84, 36.92, 37.46,
+ 29.97, 30.85, 31.97, 39.69, 24.23, 39.68, 23.23, 25.65, 24.1, 24.1,
+ 32.36, 20.77, 31.48, 30.43, 34.57, 25, 31.86, 28.37, 27.57, 34.51,
+ 34.21, 30.35, 35.37, 34.26, 20.19, 31.11, 25.46, 30.85, 38.28, 39.18,
+ 24.94, 33.61, 35.48, 29.19, 28.47, 34.24, 27.98, 30.65, 33.31, 29.11,
+ 27.67, 28.15, 31.89, 27.31, 35.36, 31.32, 21.37, 37.06, 22.86, 31.19,
+ 35.86, 39.63, 26.08, 20.91, 29.95, 29.73, 31.83, 25.12, 26.33, 35.75,
+ 36.35, 29.32, 32.93, 36.28, 35.15, 29.16, 34.95, 32.23, 39.83, 34,
+ 38.82, 39.27, 28.1, 38.03, 33.04, 21.13, 39.43, 27.71, 28.92, 38.88,
+ 31.44, 22.45, 21.88, 21.29, 37.96, 34.59, 23.76, 21.46, 28.25, 30.01,
+ 32.67, 30.87, 21.45, 27.07, 29.95, 23.76, 34.45, 33.57, 29.51, 28.85,
+ 20.49, 27.32, 36.09, 27.55, 22.49, 27.33, 22.91, 34.39, 21.86, 28.62,
+ 24.02, 29.98, 29.55, 35.6, 26.92, 26.47, 34.07, 28.36, 35.97, 30.4,
+ 21.42, 37.06, 23.01, 31.24, 29.91, 22.98, 36.08, 26.11, 24.5, 28.71,
+ 21.6, 38.47, 21.23, 38.74, 27.62, 35.12, 38.98, 29.2, 29.41, 22.66,
+ 29.08, 32.39, 30.73, 23.88, 20.5, 33.48, 23.09, 39.65, 27.37, 20.53,
+ 31.68, 26.79, 39.88, 25.11, 20.76, 37.81, 20.62, 30.28, 34.01, 37.88,
+ 37.66, 23.44, 23.2, 35.33, 27.54, 33.79, 38.84, 33.4, 27.97, 28.14,
+ 30.23, 30.13, 25.01, 22.84, 26.51, 26.62, 21.51, 32.1, 33.69, 32.75,
+ 27.17, 35.47, 26.46, 35.13, 23.52, 26.48, 35.51, 39.39, 36.94, 34.34,
+ 22.69, 24.81, 34.26, 33.41, 20.04, 38.11, 30.14, 29.66, 35.93, 31.41,
+ 22.67, 20.44, 22.99, 31.01, 20.39, 35.9, 20.01, 31.84, 20.56
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/anticheat/value-pool.json b/packages/typing-fixtures/reference/anticheat/value-pool.json
new file mode 100644
index 000000000..12092e09d
--- /dev/null
+++ b/packages/typing-fixtures/reference/anticheat/value-pool.json
@@ -0,0 +1,51 @@
+{
+ "name": "value-pool",
+ "label": "bot",
+ "client": "web",
+ "description": "Delays drawn from small hardcoded pools.",
+ "samples": [
+ {
+ "keySpacing": [
+ 73, 73, 104, 95, 88, 88, 88, 81, 88, 95, 81, 73, 95, 88, 73, 81, 81, 88,
+ 81, 104, 81, 104, 73, 73, 73, 81, 88, 95, 73, 104, 88, 95, 95, 104, 95,
+ 104, 104, 73, 73, 81, 88, 73, 73, 95, 81, 88, 88, 104, 95, 95, 81, 81,
+ 73, 73, 104, 81, 73, 95, 81, 73, 95, 104, 88, 104, 95, 104, 73, 88, 95,
+ 104, 104, 104, 88, 104, 104, 88, 104, 73, 88, 81, 95, 88, 95, 104, 88,
+ 104, 88, 95, 95, 88, 81, 81, 73, 73, 104, 104, 95, 73, 95, 88, 95, 104,
+ 81, 104, 95, 104, 95, 104, 73, 104, 88, 88, 73, 81, 81, 104, 104, 95,
+ 95, 104, 95, 95, 104, 88, 81, 88, 95, 104, 73, 88, 88, 88, 73, 81, 88,
+ 88, 73, 73, 73, 73, 73, 104, 104, 73, 73, 88, 88, 95, 81, 81, 95, 95,
+ 104, 88, 95, 88, 88, 81, 88, 95, 73, 81, 104, 104, 95, 88, 81, 104, 81,
+ 81, 88, 104, 88, 104, 88, 88, 81, 88, 81, 104, 88, 104, 104, 88, 88, 95,
+ 104, 88, 95, 73, 95, 81, 95, 104, 81, 81, 73, 95, 95, 104, 81, 81, 73,
+ 95, 104, 95, 104, 104, 81, 104, 81, 88, 81, 95, 95, 95, 95, 95, 88, 81,
+ 104, 88, 73, 95, 88, 88, 88, 104, 104, 73, 73, 81, 88, 81, 104, 104, 88,
+ 104, 81, 88, 104, 95, 95, 81, 104, 104, 104, 73, 73, 95, 73, 73, 104,
+ 81, 88, 81, 95, 95, 73, 88, 104, 73, 73, 81, 95, 95, 81, 88, 73, 88, 88,
+ 88, 73, 95, 81, 95, 104, 73, 73, 95, 104, 81, 104, 95, 81, 73, 73, 104,
+ 95, 81, 88, 73, 88, 88, 95, 73, 95, 73, 81, 81, 104, 73, 73, 104, 104,
+ 88, 88, 95, 73, 88, 73, 88, 95, 104, 73, 88, 81, 73, 104
+ ],
+ "keyDuration": [
+ 41, 41, 41, 47, 52, 52, 52, 52, 52, 52, 47, 52, 52, 41, 47, 47, 41, 41,
+ 47, 47, 52, 47, 52, 41, 52, 47, 47, 47, 52, 41, 47, 47, 47, 47, 41, 47,
+ 52, 52, 47, 52, 41, 52, 41, 52, 47, 41, 47, 41, 52, 52, 47, 52, 41, 47,
+ 41, 41, 52, 52, 41, 41, 47, 47, 41, 47, 52, 41, 47, 52, 41, 41, 47, 41,
+ 47, 47, 52, 52, 52, 47, 52, 47, 52, 47, 47, 47, 52, 52, 52, 52, 52, 52,
+ 47, 41, 41, 52, 41, 52, 41, 52, 47, 52, 41, 41, 41, 52, 47, 41, 41, 41,
+ 52, 41, 41, 41, 52, 41, 52, 41, 52, 47, 52, 47, 47, 52, 41, 47, 47, 52,
+ 47, 52, 47, 47, 41, 41, 47, 41, 52, 41, 41, 41, 47, 52, 52, 47, 52, 52,
+ 41, 41, 41, 47, 47, 41, 47, 47, 41, 41, 47, 47, 41, 41, 47, 52, 52, 47,
+ 52, 52, 52, 47, 52, 52, 52, 52, 52, 52, 41, 41, 52, 47, 52, 41, 47, 47,
+ 41, 47, 47, 52, 41, 41, 47, 47, 41, 41, 47, 41, 52, 52, 52, 52, 52, 47,
+ 52, 52, 47, 52, 52, 47, 52, 52, 47, 47, 41, 47, 52, 52, 47, 52, 52, 47,
+ 52, 47, 52, 41, 52, 52, 41, 47, 52, 52, 47, 47, 52, 52, 41, 47, 41, 47,
+ 47, 47, 47, 52, 47, 52, 41, 41, 52, 41, 52, 41, 47, 47, 52, 41, 41, 47,
+ 47, 41, 52, 41, 47, 47, 41, 52, 52, 52, 41, 41, 47, 41, 41, 47, 41, 41,
+ 47, 52, 52, 47, 47, 47, 47, 41, 41, 47, 47, 52, 41, 41, 41, 47, 52, 52,
+ 41, 52, 52, 47, 52, 52, 47, 41, 41, 52, 41, 47, 52, 52, 47, 47, 52, 52,
+ 47, 41, 41, 52, 52, 47, 47, 47, 52, 41, 41, 41, 52, 52
+ ]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/custom-repeat.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/custom-repeat.json
new file mode 100644
index 000000000..7e39dab15
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/custom-repeat.json
@@ -0,0 +1,108 @@
+{
+ "completedEvent": {
+ "wpm": 58.46,
+ "rawWpm": 69.52,
+ "charStats": [37, 1, 1, 0],
+ "charTotal": 44,
+ "acc": 95.45,
+ "language": "english",
+ "testDuration": 7.5954,
+ "lastKeyToEnd": 3.9,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "customText": {
+ "textLen": 9,
+ "mode": "repeat",
+ "pipeDelimiter": false,
+ "limit": {
+ "mode": "word",
+ "value": 9
+ }
+ },
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473740686,
+ "mode": "custom",
+ "mode2": "custom",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 90.69,
+ "wpmConsistency": 83.55,
+ "keyConsistency": 67.44,
+ "chartData": {
+ "wpm": [72, 72, 44, 48, 53, 56, 57, 58],
+ "burst": [72, 72, 72, 60, 72, 72, 60, 81],
+ "err": [0, 0, 2, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 105.4, 98.5, 178.79999999999998, 300.3, 183, 168.5999999999999,
+ 147.60000000000014, 197.79999999999995, 180.79999999999995, 247.5, 187.5,
+ 120.70000000000005, 145.69999999999982, 121.10000000000036,
+ 190.19999999999982, 139.0999999999999, 280.9000000000001,
+ 114.59999999999991, 178.20000000000027, 150.5999999999999,
+ 306.9000000000001, 167.29999999999973, 127.40000000000009,
+ 161.30000000000018, 204.69999999999982, 131.10000000000036,
+ 265.09999999999945, 190.69999999999982, 208.5, 131, 125.80000000000018,
+ 222.40000000000055, 176.69999999999982, 120.30000000000018,
+ 94.69999999999982, 336.09999999999945, 152.90000000000055,
+ 193.19999999999982, 205.30000000000018, 152.19999999999982,
+ 189.60000000000036, 105.09999999999945, 186.30000000000018
+ ],
+ "keyDuration": [
+ 119.80000000000001, 107.6, 107.9, 88.90000000000003, 64.79999999999995,
+ 121.70000000000005, 39.700000000000045, 72.29999999999995,
+ 72.70000000000005, 119, 42.799999999999955, 60.00000000000023,
+ 54.69999999999982, 148.5, 46.59999999999991, 75.30000000000018,
+ 147.80000000000018, 80.90000000000009, 137.80000000000018,
+ 137.0999999999999, 129.5, 55.09999999999991, 100.90000000000009, 126,
+ 42.399999999999636, 76.69999999999982, 135.09999999999945,
+ 57.69999999999982, 69.20000000000073, 42.80000000000018,
+ 122.30000000000018, 114.80000000000018, 47.899999999999636, 99, 142,
+ 122.30000000000018, 69.80000000000018, 134.89999999999964,
+ 102.89999999999964, 116.30000000000018, 54.19999999999982,
+ 96.59999999999945, 70.5, 53.19999999999982
+ ],
+ "keyOverlap": 91.3,
+ "client": "web"
+ },
+ "hash": "2ba5defbc48537b32b5ae97146795e49b659bbec",
+ "derived": {
+ "inputHistory": [
+ "The ",
+ "quick ",
+ "browns ",
+ "fox ",
+ "jumps ",
+ "over ",
+ "the ",
+ "lazy ",
+ "dog"
+ ],
+ "missedWords": {
+ "brown": 2
+ },
+ "wordBurstHistory": [124, 82, 93, 108, 91, 92, 123, 85, 165],
+ "correctedWordsHistory": [
+ "The ",
+ "quick ",
+ "browns ",
+ "fox ",
+ "jumps ",
+ "over ",
+ "the ",
+ "lazy ",
+ "dog"
+ ],
+ "rawHistory": [72, 72, 72, 69, 70, 70, 69, 70],
+ "keypressesPerSecond": [6, 6, 6, 5, 6, 6, 5, 4],
+ "incompleteTestSeconds": 7.6
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/quote-short.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/quote-short.json
new file mode 100644
index 000000000..b2f6805fb
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/quote-short.json
@@ -0,0 +1,122 @@
+{
+ "completedEvent": {
+ "wpm": 71.75,
+ "rawWpm": 71.75,
+ "charStats": [69, 0, 0, 0],
+ "charTotal": 69,
+ "acc": 98.57,
+ "language": "english",
+ "testDuration": 11.54,
+ "lastKeyToEnd": 2.4,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "quoteLength": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473755938,
+ "mode": "quote",
+ "mode2": "36",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 85.2,
+ "wpmConsistency": 94.8,
+ "keyConsistency": 66.97,
+ "chartData": {
+ "wpm": [60, 66, 68, 69, 72, 72, 72, 74, 71, 72, 71, 72],
+ "burst": [72, 72, 72, 72, 84, 72, 72, 84, 48, 84, 60, 89],
+ "err": [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 317.6, 91.59999999999997, 204.59999999999997, 203.9000000000001,
+ 91.29999999999995, 145.9000000000001, 165.39999999999986,
+ 137.10000000000014, 282, 119.89999999999986, 193.29999999999995, 335.5,
+ 205.0999999999999, 115.40000000000009, 115.40000000000009,
+ 116.19999999999982, 137.70000000000027, 158.5999999999999,
+ 160.0999999999999, 176.4000000000001, 122.09999999999991,
+ 189.20000000000027, 209.39999999999964, 170.60000000000036, 130, 204.5,
+ 121.89999999999964, 100.90000000000055, 99.39999999999964,
+ 148.39999999999964, 151.40000000000055, 196.89999999999964,
+ 169.40000000000055, 125, 104.39999999999964, 160.69999999999982, 175.5,
+ 108.90000000000055, 141.19999999999982, 240.39999999999964,
+ 192.30000000000018, 100.89999999999964, 238.70000000000073, 150,
+ 183.79999999999927, 121.90000000000055, 129.80000000000018,
+ 219.09999999999945, 112.69999999999982, 201.20000000000073,
+ 177.60000000000036, 164.59999999999854, 198.70000000000073, 284.5,
+ 202.79999999999927, 173.70000000000073, 136.29999999999927,
+ 114.30000000000109, 101.39999999999964, 99, 184, 263.7999999999993,
+ 207.40000000000146, 152.29999999999927, 297.60000000000036,
+ 119.79999999999927, 156, 189.3000000000011, 121.39999999999964
+ ],
+ "keyDuration": [
+ 54.5, 48.5, 141.50000000000006, 63.200000000000045, 81, 46.60000000000002,
+ 125.79999999999995, 92.90000000000009, 64.09999999999991,
+ 143.29999999999995, 144.9000000000001, 150.5, 144.20000000000027,
+ 143.70000000000027, 101.40000000000009, 71.90000000000009,
+ 132.4000000000001, 105.09999999999991, 136.9000000000001,
+ 136.30000000000018, 102.90000000000009, 135.20000000000027, 62, 83, 44,
+ 72.89999999999964, 72.89999999999964, 79.5, 78.89999999999964,
+ 142.89999999999964, 53.5, 91, 89.19999999999982, 72.29999999999927, 141.5,
+ 58.30000000000018, 114, 139.19999999999982, 105.89999999999964,
+ 122.39999999999964, 61.5, 45.19999999999982, 103.80000000000018,
+ 120.19999999999982, 136.79999999999927, 60, 98.30000000000018,
+ 99.39999999999964, 52.69999999999982, 84.80000000000018,
+ 121.39999999999964, 39.599999999998545, 46.30000000000109,
+ 84.20000000000073, 130, 82, 40.5, 136.40000000000146, 112.29999999999927,
+ 80.79999999999927, 61, 104.29999999999927, 70.10000000000036,
+ 141.59999999999854, 107.10000000000036, 72.79999999999927, 84, 117.5,
+ 53.19999999999891, 78.35000000000036
+ ],
+ "keyOverlap": 152.1,
+ "client": "web"
+ },
+ "hash": "80993c14b9fcec77e8f1acdfb86424af68182c93",
+ "derived": {
+ "inputHistory": [
+ "Courage ",
+ "is ",
+ "resistance ",
+ "to ",
+ "fear, ",
+ "mastery ",
+ "of ",
+ "fear ",
+ "- ",
+ "not ",
+ "absence ",
+ "of ",
+ "fear."
+ ],
+ "missedWords": {
+ "Courage": 1
+ },
+ "wordBurstHistory": [
+ 71, 115, 88, 120, 116, 98, 123, 103, 214, 89, 95, 100, 123
+ ],
+ "correctedWordsHistory": [
+ "xourage ",
+ "is ",
+ "resistance ",
+ "to ",
+ "fear, ",
+ "mastery ",
+ "of ",
+ "fear ",
+ "- ",
+ "not ",
+ "absence ",
+ "of ",
+ "fear."
+ ],
+ "rawHistory": [60, 66, 68, 69, 72, 72, 72, 74, 71, 72, 71, 72],
+ "keypressesPerSecond": [6, 6, 6, 6, 7, 6, 6, 7, 4, 7, 5, 4],
+ "incompleteTestSeconds": 11.54
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/time-15.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/time-15.json
new file mode 100644
index 000000000..b5308554c
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/time-15.json
@@ -0,0 +1,128 @@
+{
+ "completedEvent": {
+ "wpm": 54.36,
+ "rawWpm": 59.96,
+ "charStats": [68, 3, 2, 0],
+ "charTotal": 75,
+ "acc": 93.33,
+ "language": "english",
+ "testDuration": 15.01,
+ "lastKeyToEnd": 118.4,
+ "startToFirstKey": 0,
+ "afkDuration": 1,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473720263,
+ "mode": "time",
+ "mode2": "15",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 64.99,
+ "wpmConsistency": 81.89,
+ "keyConsistency": 7.22,
+ "chartData": {
+ "wpm": [84, 72, 72, 60, 53, 54, 57, 50, 47, 47, 49, 51, 53, 53, 54],
+ "burst": [84, 60, 72, 60, 72, 60, 72, 0, 24, 48, 72, 72, 72, 60, 72],
+ "err": [0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 135.2, 183.90000000000003, 147.79999999999995, 121.30000000000007,
+ 227.29999999999995, 176.79999999999995, 184.29999999999995,
+ 128.9000000000001, 121.5, 287.5, 154.29999999999995, 196.60000000000014,
+ 200.4000000000001, 139.0999999999999, 242.4000000000001, 179,
+ 87.89999999999964, 192.9000000000001, 173.5999999999999,
+ 298.3000000000002, 182.0999999999999, 158, 101.5, 154.9000000000001,
+ 137.10000000000036, 152.59999999999945, 287, 171.69999999999982,
+ 150.30000000000018, 190.80000000000018, 299.6999999999998,
+ 182.40000000000055, 98.5, 192.39999999999964, 157.19999999999982,
+ 227.30000000000018, 159.80000000000018, 181.5, 203.39999999999964,
+ 1901.5999999999995, 98.20000000000073, 108.60000000000036,
+ 183.6999999999989, 205.90000000000146, 372, 195, 188.79999999999927,
+ 164.79999999999927, 136.5, 222.40000000000146, 153.89999999999964,
+ 198.10000000000036, 192.09999999999854, 140.90000000000146,
+ 241.89999999999964, 104.29999999999927, 205.20000000000073,
+ 113.60000000000036, 109.89999999999964, 288.5, 171.5, 105.29999999999927,
+ 104.30000000000109, 288.09999999999854, 177, 204, 205.40000000000146,
+ 136.59999999999854, 141.20000000000073, 235.29999999999927,
+ 90.30000000000109, 184.5, 143.39999999999964, 208.60000000000036
+ ],
+ "keyDuration": [
+ 59.6, 48.400000000000006, 92.79999999999995, 58.39999999999998,
+ 100.89999999999998, 48.700000000000045, 90.40000000000009,
+ 134.20000000000005, 115.79999999999995, 116.5, 132.0999999999999,
+ 40.299999999999955, 73.29999999999973, 126.39999999999964,
+ 121.19999999999982, 81, 37, 92.60000000000036, 106.20000000000027,
+ 125.40000000000009, 103, 134.30000000000018, 107.59999999999991,
+ 80.90000000000009, 132.30000000000018, 92.39999999999964,
+ 102.40000000000055, 80, 118.90000000000055, 144.80000000000018,
+ 119.30000000000018, 145.5, 46.099999999999454, 98.5, 76.60000000000036,
+ 70.80000000000018, 104.30000000000018, 82.80000000000018,
+ 58.30000000000018, 111.10000000000036, 124.5, 83.5, 117.89999999999964,
+ 106.5, 150, 105.5, 146.59999999999854, 74.5, 144.5, 105.5,
+ 108.39999999999964, 70.39999999999964, 46.399999999999636,
+ 49.70000000000073, 137.39999999999964, 111.5, 68.80000000000109,
+ 99.89999999999964, 40.399999999999636, 110.39999999999964,
+ 130.10000000000036, 103.29999999999927, 21.899999999999636,
+ 106.09999999999854, 104.30000000000109, 75.30000000000109,
+ 49.80000000000109, 60, 94.70000000000073, 100.89999999999964,
+ 111.40000000000146, 90.10000000000036, 74.69999999999891,
+ 80.70000000000073, 49
+ ],
+ "keyOverlap": 74,
+ "client": "web"
+ },
+ "hash": "cef782e3482d51f43184a11f4308cf68a585acc5",
+ "derived": {
+ "inputHistory": [
+ "into ",
+ "over ",
+ "have ",
+ "look ",
+ "hphere ",
+ "not ",
+ "mean ",
+ "for ",
+ "most ",
+ "want ",
+ "lead ",
+ "much ",
+ "off ",
+ "think ",
+ "year "
+ ],
+ "missedWords": {
+ "here": 5
+ },
+ "wordBurstHistory": [
+ 102, 98, 87, 95, 95, 94, 95, 88, 101, 88, 88, 113, 126, 83, 96
+ ],
+ "correctedWordsHistory": [
+ "into ",
+ "over ",
+ "have ",
+ "look ",
+ "hphere ",
+ "not ",
+ "mean ",
+ "for ",
+ "most ",
+ "want ",
+ "lead ",
+ "much ",
+ "off ",
+ "think ",
+ "year "
+ ],
+ "rawHistory": [84, 72, 72, 69, 70, 68, 69, 60, 56, 55, 57, 58, 59, 59, 60],
+ "keypressesPerSecond": [7, 5, 6, 5, 6, 5, 6, 0, 2, 4, 6, 6, 6, 5, 6],
+ "incompleteTestSeconds": 14.01
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-clean.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-clean.json
new file mode 100644
index 000000000..c143c1116
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-clean.json
@@ -0,0 +1,99 @@
+{
+ "completedEvent": {
+ "wpm": 109.79,
+ "rawWpm": 109.79,
+ "charStats": [43, 0, 0, 0],
+ "charTotal": 43,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 4.7,
+ "lastKeyToEnd": 3.3,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473680017,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 84.03,
+ "wpmConsistency": 95.73,
+ "keyConsistency": 59.8,
+ "chartData": {
+ "wpm": [120, 108, 116, 108, 110],
+ "burst": [120, 96, 132, 84, 120],
+ "err": [0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 87.5, 113.30000000000001, 173.7, 87, 87.39999999999998,
+ 165.89999999999998, 75.70000000000005, 88.70000000000005,
+ 84.39999999999998, 77.60000000000002, 132.5999999999999,
+ 93.20000000000005, 80.29999999999995, 169.4000000000001,
+ 102.79999999999995, 105.29999999999995, 105.40000000000009,
+ 237.20000000000005, 67.19999999999982, 82.90000000000009,
+ 72.69999999999982, 93.40000000000009, 75.59999999999991, 154,
+ 73.60000000000036, 100.89999999999964, 78.60000000000036,
+ 115.29999999999973, 259.0999999999999, 110.40000000000009,
+ 67.09999999999991, 216.60000000000036, 113.19999999999982,
+ 117.40000000000009, 67.19999999999982, 117.70000000000027,
+ 119.80000000000018, 82.5, 144.89999999999964, 99.19999999999982, 92,
+ 106.40000000000055
+ ],
+ "keyDuration": [
+ 37, 143.8, 71.69999999999999, 119.69999999999999, 149.70000000000005,
+ 92.80000000000007, 84.5, 56.5, 46.59999999999991, 124.89999999999998,
+ 60.09999999999991, 86.70000000000005, 129.9000000000001,
+ 61.299999999999955, 99.79999999999995, 62.5, 87.79999999999995,
+ 150.0999999999999, 122.90000000000009, 75.80000000000018,
+ 69.09999999999991, 118.5, 55, 45.5, 134.60000000000036, 151,
+ 83.10000000000036, 114.09999999999991, 145.20000000000027,
+ 138.30000000000018, 157.4000000000001, 42.70000000000027,
+ 72.09999999999991, 96, 114.90000000000009, 115, 56.399999999999636,
+ 56.099999999999454, 51.5, 74.39999999999964, 95.5, 46.400000000000546,
+ 104.72999999999956
+ ],
+ "keyOverlap": 549.4,
+ "client": "web"
+ },
+ "hash": "fea534e2d7a9e8cbc02838fdf7f336aaf4566602",
+ "derived": {
+ "inputHistory": [
+ "of ",
+ "by ",
+ "come ",
+ "as ",
+ "can ",
+ "never ",
+ "they ",
+ "by ",
+ "system ",
+ "most"
+ ],
+ "missedWords": {},
+ "wordBurstHistory": [177, 206, 183, 208, 154, 184, 164, 203, 136, 202],
+ "correctedWordsHistory": [
+ "of ",
+ "by ",
+ "come ",
+ "as ",
+ "can ",
+ "never ",
+ "they ",
+ "by ",
+ "system ",
+ "most"
+ ],
+ "rawHistory": [120, 108, 116, 108, 110],
+ "keypressesPerSecond": [10, 8, 11, 7, 7],
+ "incompleteTestSeconds": 4.7
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-errors.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-errors.json
new file mode 100644
index 000000000..411162781
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-errors.json
@@ -0,0 +1,110 @@
+{
+ "completedEvent": {
+ "wpm": 52.94,
+ "rawWpm": 63.53,
+ "charStats": [45, 1, 2, 0],
+ "charTotal": 54,
+ "acc": 91.38,
+ "language": "english",
+ "testDuration": 10.2,
+ "lastKeyToEnd": 3.7,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473701467,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 83.63,
+ "wpmConsistency": 86.18,
+ "keyConsistency": 67.27,
+ "chartData": {
+ "wpm": [48, 60, 64, 42, 46, 44, 43, 48, 51, 52],
+ "burst": [72, 60, 72, 84, 60, 60, 48, 84, 72, 60],
+ "err": [1, 0, 0, 3, 0, 0, 1, 0, 0, 0]
+ },
+ "keySpacing": [
+ 115, 193.7, 151.60000000000002, 277.40000000000003, 182.19999999999993,
+ 300.6, 171.4000000000001, 206.89999999999986, 251.20000000000005,
+ 106.79999999999995, 91.10000000000014, 145.5, 207.29999999999973,
+ 146.20000000000027, 183.9000000000001, 97.29999999999973, 207.5,
+ 90.40000000000009, 102.09999999999991, 145.80000000000018,
+ 175.9000000000001, 182.89999999999964, 189.20000000000027,
+ 303.2000000000003, 175.59999999999945, 191.60000000000036,
+ 151.59999999999945, 192.90000000000055, 157.39999999999964, 278,
+ 144.40000000000055, 249.79999999999927, 186.20000000000073,
+ 200.39999999999964, 113.10000000000036, 284.59999999999945,
+ 202.69999999999982, 277.5, 125.80000000000018, 250.30000000000018,
+ 133.89999999999964, 138.40000000000055, 146.89999999999964,
+ 121.90000000000055, 174.79999999999927, 233.5, 165.60000000000036,
+ 128.89999999999964, 104.10000000000036, 176.39999999999964,
+ 325.89999999999964, 159, 93.30000000000109, 152.10000000000036,
+ 192.09999999999854, 150.5, 195
+ ],
+ "keyDuration": [
+ 72.4, 99.4, 111, 80.40000000000003, 52.19999999999993, 61.5,
+ 57.09999999999991, 78.39999999999986, 43.700000000000045, 46,
+ 59.90000000000009, 143.0999999999999, 107.69999999999982,
+ 66.20000000000027, 76.40000000000009, 95.69999999999982,
+ 48.20000000000027, 128.30000000000018, 99.5, 45.90000000000009,
+ 64.69999999999982, 44.79999999999973, 140.9000000000001,
+ 87.69999999999982, 48.099999999999454, 147.90000000000055,
+ 108.59999999999945, 44.600000000000364, 131, 71.19999999999982,
+ 142.10000000000036, 77.19999999999982, 89.10000000000036,
+ 134.89999999999964, 86.30000000000018, 111, 118.60000000000036,
+ 63.80000000000018, 43, 129.90000000000055, 124.80000000000018,
+ 88.40000000000055, 133.89999999999964, 149.60000000000036,
+ 48.099999999999454, 42.400000000000546, 124.20000000000073, 71.5, 102.5,
+ 4.799999999999272, 148.5, 51.600000000000364, 143.20000000000073,
+ 103.29999999999927, 117.59999999999854, 85.60000000000036, 47, 89.5
+ ],
+ "keyOverlap": 115.5,
+ "client": "web"
+ },
+ "hash": "cf702ec601438e277d54d39dcfb14db01cef6d88",
+ "derived": {
+ "inputHistory": [
+ "all ",
+ "man ",
+ "would ",
+ "schoolxx ",
+ "where ",
+ "day ",
+ "do ",
+ "never ",
+ "well ",
+ "against"
+ ],
+ "missedWords": {
+ "man": 1,
+ "school": 3,
+ "do": 1
+ },
+ "wordBurstHistory": [104, 56, 103, 91, 83, 54, 59, 101, 104, 102],
+ "correctedWordsHistory": [
+ "all ",
+ "mqn ",
+ "would ",
+ "schoolxx ",
+ "where ",
+ "day ",
+ "d ",
+ "never ",
+ "well ",
+ "against"
+ ],
+ "rawHistory": [72, 60, 64, 69, 67, 62, 58, 61, 63, 62],
+ "keypressesPerSecond": [6, 5, 6, 7, 5, 5, 4, 7, 6, 5],
+ "incompleteTestSeconds": 10.2
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-punctuation-numbers.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-punctuation-numbers.json
new file mode 100644
index 000000000..5f52f8e5f
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-punctuation-numbers.json
@@ -0,0 +1,102 @@
+{
+ "completedEvent": {
+ "wpm": 73.82,
+ "rawWpm": 73.82,
+ "charStats": [51, 0, 0, 0],
+ "charTotal": 51,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 8.29,
+ "lastKeyToEnd": 3.8,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": true,
+ "numbers": true,
+ "lazyMode": false,
+ "timestamp": 1791473781136,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 90.21,
+ "wpmConsistency": 97.63,
+ "keyConsistency": 63.72,
+ "chartData": {
+ "wpm": [72, 78, 76, 75, 77, 76, 74, 74],
+ "burst": [72, 84, 72, 72, 84, 72, 60, 72],
+ "err": [0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 117.1, 159.6, 288.90000000000003, 150.79999999999995, 151.70000000000005,
+ 157.39999999999998, 104.90000000000009, 331.1999999999998, 104,
+ 159.60000000000014, 179.5, 94.09999999999991, 133.50000000000023,
+ 129.39999999999964, 208.5, 127.80000000000018, 198.30000000000018, 172.5,
+ 165, 204.29999999999973, 126.59999999999991, 157.60000000000036,
+ 96.19999999999982, 186.80000000000018, 119.79999999999973,
+ 98.59999999999991, 263.10000000000036, 139.69999999999982,
+ 101.80000000000018, 191.69999999999982, 93.10000000000036,
+ 316.6999999999998, 136.30000000000018, 91.59999999999945, 225,
+ 90.90000000000055, 133.39999999999964, 209.39999999999964,
+ 191.10000000000036, 170.39999999999964, 284.2000000000007,
+ 159.79999999999927, 129.80000000000018, 128.90000000000055,
+ 124.09999999999945, 282.1999999999998, 178, 176.30000000000018,
+ 134.60000000000036, 207.9999999999991
+ ],
+ "keyDuration": [
+ 105.39999999999999, 115.80000000000001, 89.30000000000001,
+ 65.10000000000002, 46.39999999999998, 47, 63.299999999999955, 149.5,
+ 50.200000000000045, 44, 87.79999999999995, 123, 90.10000000000014, 95.5,
+ 94.70000000000027, 131.9000000000001, 121, 94.89999999999964, 121,
+ 75.29999999999973, 51.5, 151.70000000000027, 84.79999999999973,
+ 90.40000000000009, 147.89999999999964, 134.70000000000027, 100.5, 60,
+ 52.100000000000364, 150.19999999999982, 60.899999999999636, 143.5,
+ 126.39999999999964, 90.09999999999945, 89.5, 116.30000000000018,
+ 122.69999999999982, 70.80000000000018, 138.20000000000073,
+ 104.80000000000018, 146.30000000000018, 49, 83.20000000000073,
+ 135.10000000000036, 98.79999999999927, 119.60000000000036,
+ 84.10000000000036, 96.80000000000018, 132.60000000000036,
+ 70.19999999999982, 46.30000000000109
+ ],
+ "keyOverlap": 128.8,
+ "client": "web"
+ },
+ "hash": "b0c6102597a3924a5c67d4b412d2c0896366bae1",
+ "derived": {
+ "inputHistory": [
+ "25 ",
+ "for. ",
+ "Before ",
+ "turn ",
+ "school ",
+ "line ",
+ "34 ",
+ "early ",
+ "not, ",
+ "most."
+ ],
+ "missedWords": {},
+ "wordBurstHistory": [129, 106, 105, 91, 107, 114, 159, 91, 111, 103],
+ "correctedWordsHistory": [
+ "25 ",
+ "for. ",
+ "Before ",
+ "turn ",
+ "school ",
+ "line ",
+ "34 ",
+ "early ",
+ "not, ",
+ "most."
+ ],
+ "rawHistory": [72, 78, 76, 75, 77, 76, 74, 74],
+ "keypressesPerSecond": [6, 7, 6, 6, 7, 6, 5, 6],
+ "incompleteTestSeconds": 8.29
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-stop-on-letter.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-stop-on-letter.json
new file mode 100644
index 000000000..47fb81ee8
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/words-10-stop-on-letter.json
@@ -0,0 +1,112 @@
+{
+ "completedEvent": {
+ "wpm": 65.79,
+ "rawWpm": 65.79,
+ "charStats": [54, 0, 0, 0],
+ "charTotal": 54,
+ "acc": 90,
+ "language": "english",
+ "testDuration": 9.85,
+ "lastKeyToEnd": 3.2,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473769303,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": true,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 79.41,
+ "wpmConsistency": 95.35,
+ "keyConsistency": 71.11,
+ "chartData": {
+ "wpm": [72, 72, 72, 72, 70, 68, 67, 65, 63, 66],
+ "burst": [72, 96, 72, 72, 84, 60, 60, 72, 48, 99],
+ "err": [0, 2, 0, 0, 2, 0, 0, 2, 0, 0]
+ },
+ "keySpacing": [
+ 197.6, 153.4, 145.5, 155.60000000000002, 251, 180.89999999999998,
+ 114.09999999999991, 124.40000000000009, 126.40000000000009,
+ 120.89999999999986, 113.90000000000009, 108.09999999999991,
+ 155.9000000000001, 234.89999999999986, 154.20000000000027,
+ 173.5999999999999, 164.5, 190.0999999999999, 110.19999999999982,
+ 141.60000000000036, 150, 132.5, 147.5999999999999, 172.5,
+ 129.5999999999999, 198.80000000000018, 200.09999999999945,
+ 112.40000000000055, 127.09999999999945, 175.60000000000036,
+ 154.89999999999964, 167.80000000000018, 255.10000000000036,
+ 168.89999999999964, 204.69999999999982, 161.30000000000018,
+ 206.60000000000036, 297.6999999999998, 152.5, 177, 179.69999999999982,
+ 155.19999999999982, 284.8000000000002, 180.80000000000018,
+ 126.60000000000036, 90.5, 204.09999999999945, 165, 213.59999999999945,
+ 176.10000000000036, 123.70000000000073, 326.60000000000036,
+ 164.29999999999927, 104.79999999999927, 150.10000000000036,
+ 109.20000000000073, 118.29999999999927, 192.89999999999964,
+ 168.90000000000146
+ ],
+ "keyDuration": [
+ 55, 117.00000000000003, 85.80000000000001, 66.39999999999998,
+ 120.39999999999998, 56.799999999999955, 96.79999999999995,
+ 79.30000000000018, 99, 148.29999999999995, 136.9000000000001,
+ 52.799999999999955, 73.40000000000009, 125.39999999999986,
+ 139.0999999999999, 108.59999999999991, 145.4000000000001, 141,
+ 42.80000000000018, 79.60000000000036, 40.79999999999973,
+ 122.89999999999964, 86, 143, 117.5, 81.5, 54.80000000000018,
+ 75.20000000000073, 111.09999999999945, 47.5, 141.5, 131.60000000000036,
+ 141.80000000000018, 96.69999999999982, 138.40000000000055,
+ 131.40000000000055, 146.69999999999982, 98.59999999999945, 39, 75,
+ 97.60000000000036, 147.30000000000018, 133.20000000000073,
+ 112.60000000000036, 101, 66.79999999999927, 42.19999999999982,
+ 131.69999999999982, 41.69999999999982, 107.30000000000109,
+ 148.89999999999964, 148.39999999999964, 127.69999999999891,
+ 129.79999999999927, 149.20000000000073, 105, 87.5, 129.5,
+ 121.30000000000109, 79
+ ],
+ "keyOverlap": 100.6,
+ "client": "web"
+ },
+ "hash": "d782f60da839d7fc27d0e0013c2f2cfc468f9087",
+ "derived": {
+ "inputHistory": [
+ "will ",
+ "people ",
+ "become ",
+ "open ",
+ "such ",
+ "hold ",
+ "what ",
+ "one ",
+ "at ",
+ "possible"
+ ],
+ "missedWords": {
+ "people": 2,
+ "such": 2,
+ "one": 2
+ },
+ "wordBurstHistory": [92, 80, 90, 103, 64, 81, 90, 63, 120, 107],
+ "correctedWordsHistory": [
+ "will ",
+ "people ",
+ "become ",
+ "open ",
+ "such ",
+ "hold ",
+ "what ",
+ "one ",
+ "at ",
+ "possible"
+ ],
+ "rawHistory": [72, 72, 72, 72, 70, 68, 67, 65, 63, 66],
+ "keypressesPerSecond": [6, 8, 6, 6, 7, 5, 5, 6, 4, 7],
+ "incompleteTestSeconds": 9.85
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/__snapshots__/zen.json b/packages/typing-fixtures/reference/keystrokes/__snapshots__/zen.json
new file mode 100644
index 000000000..aa44ca54f
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/__snapshots__/zen.json
@@ -0,0 +1,70 @@
+{
+ "completedEvent": {
+ "wpm": 68.42,
+ "rawWpm": 68.42,
+ "charStats": [26, 0, 0, 0],
+ "charTotal": 26,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 4.56,
+ "lastKeyToEnd": 0,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473729386,
+ "mode": "zen",
+ "mode2": "zen",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 86.36,
+ "wpmConsistency": 90.89,
+ "keyConsistency": 62.49,
+ "chartData": {
+ "wpm": [84, 84, 76, 69, 68],
+ "burst": [84, 84, 72, 60, 64],
+ "err": [0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 105.7, 117.8, 100.80000000000001, 165, 181.40000000000003, 180,
+ 149.89999999999998, 141.4999999999999, 178.80000000000018, 208,
+ 131.5999999999999, 122.59999999999991, 153.20000000000005,
+ 145.39999999999986, 148.10000000000036, 183, 186, 130.39999999999964,
+ 97.10000000000036, 423.0999999999999, 130.19999999999982,
+ 169.30000000000018, 94.69999999999982, 208.0999999999999,
+ 147.4000000000001, 174.4000000000001, 206.30000000000018,
+ 180.59999999999945
+ ],
+ "keyDuration": [
+ 73.7, 53.2, 98.60000000000002, 147.5, 116.09999999999997,
+ 82.09999999999991, 135.0999999999999, 122.89999999999998,
+ 95.90000000000009, 143.29999999999995, 92.59999999999991, 126.5,
+ 121.40000000000009, 78.70000000000005, 39.80000000000018,
+ 100.89999999999964, 48.29999999999973, 84.19999999999982,
+ 101.20000000000027, 108, 137.69999999999982, 60.40000000000009,
+ 93.69999999999982, 126.70000000000027, 39.30000000000018,
+ 142.50000000000045, 100.30000000000018, 72.89999999999964,
+ 22.400000000000546
+ ],
+ "keyOverlap": 15.5,
+ "client": "web"
+ },
+ "hash": "f84db70cdbf3cb6053a2cfb617cea4b16418ea51",
+ "derived": {
+ "inputHistory": ["hello ", "there ", "zen\n", "mode ", "rocks"],
+ "missedWords": {},
+ "wordBurstHistory": [107, 89, 107, 53, 98],
+ "correctedWordsHistory": ["hello ", "there ", "zen\n", "moed ", "rocks"],
+ "rawHistory": [84, 84, 76, 69, 68],
+ "keypressesPerSecond": [7, 7, 6, 5, 3],
+ "incompleteTestSeconds": 4.56
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/custom-repeat.json b/packages/typing-fixtures/reference/keystrokes/custom-repeat.json
new file mode 100644
index 000000000..01e194b4c
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/custom-repeat.json
@@ -0,0 +1,1645 @@
+{
+ "name": "custom-repeat",
+ "description": "custom text, repeat, word limit 9",
+ "recordedWith": "web",
+ "config": {
+ "mode": "custom",
+ "time": 30,
+ "words": 50,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "customText": {
+ "text": [
+ "The",
+ "quick",
+ "brown",
+ "fox",
+ "jumps",
+ "over",
+ "the",
+ "lazy",
+ "dog"
+ ],
+ "mode": "repeat",
+ "limit": {
+ "value": 9,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "T",
+ "gap": 173,
+ "hold": 125
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 116,
+ "hold": 109
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 100,
+ "hold": 105
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 176,
+ "hold": 90
+ },
+ {
+ "kind": "key",
+ "key": "q",
+ "gap": 199,
+ "hold": 65
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 184,
+ "hold": 121
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 169,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 146,
+ "hold": 73
+ },
+ {
+ "kind": "key",
+ "key": "k",
+ "gap": 198,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 181,
+ "hold": 120
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 120,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 185,
+ "hold": 63
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 123,
+ "hold": 54
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 146,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 119,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 192,
+ "hold": 75
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 139,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 123,
+ "hold": 82
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 116,
+ "hold": 136
+ },
+ {
+ "kind": "key",
+ "key": "x",
+ "gap": 177,
+ "hold": 137
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 151,
+ "hold": 129
+ },
+ {
+ "kind": "key",
+ "key": "j",
+ "gap": 168,
+ "hold": 55
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 166,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 128,
+ "hold": 126
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 163,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 202,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 132,
+ "hold": 136
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 120,
+ "hold": 59
+ },
+ {
+ "kind": "key",
+ "key": "v",
+ "gap": 190,
+ "hold": 71
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 210,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 132,
+ "hold": 123
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 128,
+ "hold": 113
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 99,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 178,
+ "hold": 98
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 120,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 96,
+ "hold": 121
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 204,
+ "hold": 71
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 154,
+ "hold": 134
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 192,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 205,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 155,
+ "hold": 52
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 125,
+ "hold": 98
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 109,
+ "hold": 68
+ },
+ {
+ "kind": "key",
+ "key": "g",
+ "gap": 182,
+ "hold": 55
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -6.9,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "T",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "T"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473732956
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 105.4,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 106.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "Th"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 112.9,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 203.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 205,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "The"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 213,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 311.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 382.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 385.9,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "The ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 471.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 683,
+ "data": {
+ "code": "KeyQ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 685.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "q",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "q"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 747.8,
+ "data": {
+ "code": "KeyQ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 866,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 867.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "qu"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 987.7,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1003.2,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 3.2
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1034.6,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1036.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "qui"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1074.3,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1182.2,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1184.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "quic"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1254.5,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1380,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1382.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "k",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": "quick"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1452.7,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1560.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1563.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 5,
+ "inputValue": "quick ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1679.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1808.3,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1811,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "b"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1851.1,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1995.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1998.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "br"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2002.8,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 2.8
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2055.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2116.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2118.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "bro"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2171.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2262.2,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2264.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "brow"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2383.3,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2385.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "brown"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2410.7,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2429.9,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2573.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2577.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": false,
+ "wordIndex": 2,
+ "charIndex": 5,
+ "inputValue": "browns"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2648.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2712.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2714.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": false,
+ "wordIndex": 2,
+ "charIndex": 6,
+ "inputValue": "browns ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2860.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2993.5,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2995.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "f"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3002.6,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 2.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3074.4,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3108.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3109.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "fo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3245.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3286.3,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3288.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "x",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "fox"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3423.4,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3436.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3439,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "fox ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3566.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3743.8,
+ "data": {
+ "code": "KeyJ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3745.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "j",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "j"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3798.9,
+ "data": {
+ "code": "KeyJ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3911.1,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3913.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "ju"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4002.9,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 2.9
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4012,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4038.5,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4040.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "jum"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4164.5,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4199.8,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4202,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "jump"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4242.2,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4404.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4406.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "jumps"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4481.2,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4535.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4537.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 5,
+ "inputValue": "jumps ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4670.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4800.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4803,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4858.4,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4991.4,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4993.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "v",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "ov"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5002.9,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 2.9
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5060.6,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5199.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5202.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "ove"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5242.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5330.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5331.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "over"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5453.2,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5456.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5457.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 4,
+ "inputValue": "over ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5571.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5679.1,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5681.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5727,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5855.8,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5856.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "th"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5954.8,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5976.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5977.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "the"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6003,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6070.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6071.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 3,
+ "inputValue": "the ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6118.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6193.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6406.9,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6409.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "l"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6476.7,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6559.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6562,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "la"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6694.7,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6753,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6755.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "laz"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6855.9,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6958.3,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6960.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "lazy"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7002.9,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 2.9
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7074.6,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7110.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7111.9,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 4,
+ "inputValue": "lazy ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7164.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7300.1,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7302.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "d",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7396.7,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7405.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7406.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "do",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7475.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7591.5,
+ "data": {
+ "code": "KeyG"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7594,
+ "data": {
+ "inputType": "insertText",
+ "data": "g",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "dog",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7595.4,
+ "data": {
+ "event": "end",
+ "timer": 7,
+ "date": 1791473740549
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7644.7,
+ "data": {
+ "code": "KeyG"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "The ",
+ "quick ",
+ "brown ",
+ "fox ",
+ "jumps ",
+ "over ",
+ "the ",
+ "lazy ",
+ "dog"
+ ],
+ "mode": "custom",
+ "mode2": "custom",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "customTextLimitMode": "word",
+ "customTextLimitValue": 9
+ }
+ },
+ "completedEvent": {
+ "wpm": 58.46,
+ "rawWpm": 69.52,
+ "charStats": [37, 1, 1, 0],
+ "charTotal": 44,
+ "acc": 95.45,
+ "language": "english",
+ "testDuration": 7.5954,
+ "lastKeyToEnd": 3.9,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "customText": {
+ "textLen": 9,
+ "mode": "repeat",
+ "pipeDelimiter": false,
+ "limit": {
+ "mode": "word",
+ "value": 9
+ }
+ },
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473740686,
+ "mode": "custom",
+ "mode2": "custom",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 90.69,
+ "wpmConsistency": 83.55,
+ "keyConsistency": 67.44,
+ "chartData": {
+ "wpm": [72, 72, 44, 48, 53, 56, 57, 58],
+ "burst": [72, 72, 72, 60, 72, 72, 60, 81],
+ "err": [0, 0, 2, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 105.4, 98.5, 178.79999999999998, 300.3, 183, 168.5999999999999,
+ 147.60000000000014, 197.79999999999995, 180.79999999999995, 247.5, 187.5,
+ 120.70000000000005, 145.69999999999982, 121.10000000000036,
+ 190.19999999999982, 139.0999999999999, 280.9000000000001,
+ 114.59999999999991, 178.20000000000027, 150.5999999999999,
+ 306.9000000000001, 167.29999999999973, 127.40000000000009,
+ 161.30000000000018, 204.69999999999982, 131.10000000000036,
+ 265.09999999999945, 190.69999999999982, 208.5, 131, 125.80000000000018,
+ 222.40000000000055, 176.69999999999982, 120.30000000000018,
+ 94.69999999999982, 336.09999999999945, 152.90000000000055,
+ 193.19999999999982, 205.30000000000018, 152.19999999999982,
+ 189.60000000000036, 105.09999999999945, 186.30000000000018
+ ],
+ "keyDuration": [
+ 119.80000000000001, 107.6, 107.9, 88.90000000000003, 64.79999999999995,
+ 121.70000000000005, 39.700000000000045, 72.29999999999995,
+ 72.70000000000005, 119, 42.799999999999955, 60.00000000000023,
+ 54.69999999999982, 148.5, 46.59999999999991, 75.30000000000018,
+ 147.80000000000018, 80.90000000000009, 137.80000000000018,
+ 137.0999999999999, 129.5, 55.09999999999991, 100.90000000000009, 126,
+ 42.399999999999636, 76.69999999999982, 135.09999999999945,
+ 57.69999999999982, 69.20000000000073, 42.80000000000018,
+ 122.30000000000018, 114.80000000000018, 47.899999999999636, 99, 142,
+ 122.30000000000018, 69.80000000000018, 134.89999999999964,
+ 102.89999999999964, 116.30000000000018, 54.19999999999982,
+ 96.59999999999945, 70.5, 53.19999999999982
+ ],
+ "keyOverlap": 91.3,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/quote-short.json b/packages/typing-fixtures/reference/keystrokes/quote-short.json
new file mode 100644
index 000000000..723b4ddbd
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/quote-short.json
@@ -0,0 +1,2523 @@
+{
+ "name": "quote-short",
+ "description": "short quote with one typo",
+ "recordedWith": "web",
+ "config": {
+ "mode": "quote",
+ "time": 30,
+ "words": 50,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [0]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "x",
+ "gap": 153,
+ "hold": 40
+ },
+ {
+ "kind": "key",
+ "key": "Backspace",
+ "gap": 170,
+ "hold": 62
+ },
+ {
+ "kind": "key",
+ "key": "C",
+ "gap": 155,
+ "hold": 48
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 91,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 206,
+ "hold": 63
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 202,
+ "hold": 82
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 93,
+ "hold": 46
+ },
+ {
+ "kind": "key",
+ "key": "g",
+ "gap": 144,
+ "hold": 126
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 166,
+ "hold": 93
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 139,
+ "hold": 62
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 209,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 122,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 192,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 174,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 205,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 116,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 115,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 116,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 139,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 159,
+ "hold": 137
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 159,
+ "hold": 136
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 176,
+ "hold": 103
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 123,
+ "hold": 134
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 187,
+ "hold": 62
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 140,
+ "hold": 80
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 169,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 130,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 121,
+ "hold": 73
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 122,
+ "hold": 81
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 101,
+ "hold": 80
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 100,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": ",",
+ "gap": 150,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 149,
+ "hold": 92
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 97,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 170,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 125,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 104,
+ "hold": 58
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 160,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 178,
+ "hold": 139
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 109,
+ "hold": 105
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 141,
+ "hold": 120
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 108,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 191,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 102,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 126,
+ "hold": 121
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 150,
+ "hold": 137
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 183,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 123,
+ "hold": 98
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 130,
+ "hold": 99
+ },
+ {
+ "kind": "key",
+ "key": "-",
+ "gap": 109,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 113,
+ "hold": 85
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 107,
+ "hold": 122
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 177,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 165,
+ "hold": 46
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 198,
+ "hold": 86
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 189,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 202,
+ "hold": 83
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 174,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 137,
+ "hold": 138
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 115,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 101,
+ "hold": 82
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 100,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 183,
+ "hold": 105
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 150,
+ "hold": 70
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 205,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 155,
+ "hold": 106
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 181,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 121,
+ "hold": 83
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 155,
+ "hold": 119
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 192,
+ "hold": 52
+ },
+ {
+ "kind": "key",
+ "key": ".",
+ "gap": 120,
+ "hold": 135
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -5.8,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "x",
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "x"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473744269
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 48.7,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 164.4,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": ""
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 317.6,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 319.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "C",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "C"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 366.1,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 409.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 411.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "Co"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 550.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 613.8,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 615.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "Cou"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 677,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 817.7,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 820.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "Cour"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 898.7,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 909,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 910.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 4,
+ "inputValue": "Coura"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 955.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1006.1,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 6.1
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1054.9,
+ "data": {
+ "code": "KeyG"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1057.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "g",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 5,
+ "inputValue": "Courag"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1180.7,
+ "data": {
+ "code": "KeyG"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1220.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1222.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 6,
+ "inputValue": "Courage"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1313.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1357.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1358.9,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 7,
+ "inputValue": "Courage ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1421.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1639.4,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1640,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "i"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1759.3,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1759.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "is"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1782.7,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1904.2,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1952.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1953.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "is ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2006.1,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 6.1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2103.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2288.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2289.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "r"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2432.3,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2493.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2494.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "re"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2608.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2609.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "res"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2636.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2710,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2724,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2725.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "resi"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2795.9,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2840.2,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2841.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "resis"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2972.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2977.9,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2979.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 5,
+ "inputValue": "resist"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3005.2,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 5.2
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3083,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3136.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3137,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 6,
+ "inputValue": "resista"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3273.4,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3296.6,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3297.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 7,
+ "inputValue": "resistan"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3432.9,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3473,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3473.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 8,
+ "inputValue": "resistanc"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3575.9,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3595.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3595.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 9,
+ "inputValue": "resistance"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3730.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3784.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3786.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 10,
+ "inputValue": "resistance ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3846.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3993.7,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3996.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4005.7,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 5.7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4076.7,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4164.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4165.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "to"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4208.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4294.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4295.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "to ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4367.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4498.8,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4501,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "f"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4571.7,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4620.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4623,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "fe"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4700.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4721.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4723.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "fea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4800.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4821,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4821.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "fear"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4963.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4969.4,
+ "data": {
+ "code": "Comma"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4971.2,
+ "data": {
+ "inputType": "insertText",
+ "data": ",",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "fear,"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5005.4,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 5.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5022.9,
+ "data": {
+ "code": "Comma"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5120.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5123,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 5,
+ "inputValue": "fear, ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5211.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5317.7,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5319.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5406.9,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5487.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5489.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "ma"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5559.4,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5612.1,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5613.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "mas"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5716.5,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5718.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "mast"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5753.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5774.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5877.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5879.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 4,
+ "inputValue": "maste"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5991.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6005.9,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 5.9
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6052.7,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6053.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 5,
+ "inputValue": "master"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6161.6,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6162.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 6,
+ "inputValue": "mastery"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6191.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6267.5,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6302.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6304,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 7,
+ "inputValue": "mastery ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6425.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6543.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6545.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6604.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6735.5,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6737.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "of"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6780.7,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6836.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6838.5,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "of ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6940.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7005.7,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 5.7
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7075.1,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7077.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "f"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7195.3,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7225.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7227.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "fe"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7361.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7408.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7411.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "fea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7468.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7530.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7532.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "fear"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7629.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7660.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7662.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 4,
+ "inputValue": "fear ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7760,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7879.7,
+ "data": {
+ "code": "Minus"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7882.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "-",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "-"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7932.4,
+ "data": {
+ "code": "Minus"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7992.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7994.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "- ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8006.4,
+ "data": {
+ "event": "step",
+ "timer": 8,
+ "drift": 6.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8077.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8193.6,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8196,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "n"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8315,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8371.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8373.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "no"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8410.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8535.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8538.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "not"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8582.1,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8734.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8736.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "not ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8818.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 9006.6,
+ "data": {
+ "event": "step",
+ "timer": 9,
+ "drift": 6.6
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9019,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9021.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 0,
+ "inputValue": "a"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9149,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9221.8,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9224.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 1,
+ "inputValue": "ab"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9303.8,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9395.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9397.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 2,
+ "inputValue": "abs"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9436,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9531.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9534,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 3,
+ "inputValue": "abse"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9646.1,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9647.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 4,
+ "inputValue": "absen"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9668.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9747.5,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9749.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 5,
+ "inputValue": "absenc"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9758.4,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9828.3,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9846.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9847.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 6,
+ "inputValue": "absence"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9907.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 10008.5,
+ "data": {
+ "event": "step",
+ "timer": 10,
+ "drift": 8.5
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10030.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10032.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 7,
+ "inputValue": "absence ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10134.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10294.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10295.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10364.4,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10501.7,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10504,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 1,
+ "inputValue": "of"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10643.3,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10654,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10656.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 2,
+ "inputValue": "of ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10761.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10951.6,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10954.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 0,
+ "inputValue": "f",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 11006.6,
+ "data": {
+ "event": "step",
+ "timer": 11,
+ "drift": 6.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11024.4,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11071.4,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11073.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 1,
+ "inputValue": "fe",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11155.4,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11227.4,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11229.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 2,
+ "inputValue": "fea",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11344.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11416.7,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11419,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 3,
+ "inputValue": "fear",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11469.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11538.1,
+ "data": {
+ "code": "Period"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11539.5,
+ "data": {
+ "inputType": "insertText",
+ "data": ".",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 4,
+ "inputValue": "fear.",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 11540.5,
+ "data": {
+ "event": "end",
+ "timer": 11,
+ "date": 1791473755808
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11616.45,
+ "data": {
+ "code": "Period",
+ "estimated": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "Courage ",
+ "is ",
+ "resistance ",
+ "to ",
+ "fear, ",
+ "mastery ",
+ "of ",
+ "fear ",
+ "- ",
+ "not ",
+ "absence ",
+ "of ",
+ "fear."
+ ],
+ "mode": "quote",
+ "mode2": "36",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 71.75,
+ "rawWpm": 71.75,
+ "charStats": [69, 0, 0, 0],
+ "charTotal": 69,
+ "acc": 98.57,
+ "language": "english",
+ "testDuration": 11.54,
+ "lastKeyToEnd": 2.4,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "quoteLength": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473755938,
+ "mode": "quote",
+ "mode2": "36",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 85.2,
+ "wpmConsistency": 94.8,
+ "keyConsistency": 66.97,
+ "chartData": {
+ "wpm": [60, 66, 68, 69, 72, 72, 72, 74, 71, 72, 71, 72],
+ "burst": [72, 72, 72, 72, 84, 72, 72, 84, 48, 84, 60, 89],
+ "err": [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 317.6, 91.59999999999997, 204.59999999999997, 203.9000000000001,
+ 91.29999999999995, 145.9000000000001, 165.39999999999986,
+ 137.10000000000014, 282, 119.89999999999986, 193.29999999999995, 335.5,
+ 205.0999999999999, 115.40000000000009, 115.40000000000009,
+ 116.19999999999982, 137.70000000000027, 158.5999999999999,
+ 160.0999999999999, 176.4000000000001, 122.09999999999991,
+ 189.20000000000027, 209.39999999999964, 170.60000000000036, 130, 204.5,
+ 121.89999999999964, 100.90000000000055, 99.39999999999964,
+ 148.39999999999964, 151.40000000000055, 196.89999999999964,
+ 169.40000000000055, 125, 104.39999999999964, 160.69999999999982, 175.5,
+ 108.90000000000055, 141.19999999999982, 240.39999999999964,
+ 192.30000000000018, 100.89999999999964, 238.70000000000073, 150,
+ 183.79999999999927, 121.90000000000055, 129.80000000000018,
+ 219.09999999999945, 112.69999999999982, 201.20000000000073,
+ 177.60000000000036, 164.59999999999854, 198.70000000000073, 284.5,
+ 202.79999999999927, 173.70000000000073, 136.29999999999927,
+ 114.30000000000109, 101.39999999999964, 99, 184, 263.7999999999993,
+ 207.40000000000146, 152.29999999999927, 297.60000000000036,
+ 119.79999999999927, 156, 189.3000000000011, 121.39999999999964
+ ],
+ "keyDuration": [
+ 54.5, 48.5, 141.50000000000006, 63.200000000000045, 81, 46.60000000000002,
+ 125.79999999999995, 92.90000000000009, 64.09999999999991,
+ 143.29999999999995, 144.9000000000001, 150.5, 144.20000000000027,
+ 143.70000000000027, 101.40000000000009, 71.90000000000009,
+ 132.4000000000001, 105.09999999999991, 136.9000000000001,
+ 136.30000000000018, 102.90000000000009, 135.20000000000027, 62, 83, 44,
+ 72.89999999999964, 72.89999999999964, 79.5, 78.89999999999964,
+ 142.89999999999964, 53.5, 91, 89.19999999999982, 72.29999999999927, 141.5,
+ 58.30000000000018, 114, 139.19999999999982, 105.89999999999964,
+ 122.39999999999964, 61.5, 45.19999999999982, 103.80000000000018,
+ 120.19999999999982, 136.79999999999927, 60, 98.30000000000018,
+ 99.39999999999964, 52.69999999999982, 84.80000000000018,
+ 121.39999999999964, 39.599999999998545, 46.30000000000109,
+ 84.20000000000073, 130, 82, 40.5, 136.40000000000146, 112.29999999999927,
+ 80.79999999999927, 61, 104.29999999999927, 70.10000000000036,
+ 141.59999999999854, 107.10000000000036, 72.79999999999927, 84, 117.5,
+ 53.19999999999891, 78.35000000000036
+ ],
+ "keyOverlap": 152.1,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/time-15.json b/packages/typing-fixtures/reference/keystrokes/time-15.json
new file mode 100644
index 000000000..762e678bc
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/time-15.json
@@ -0,0 +1,2856 @@
+{
+ "name": "time-15",
+ "description": "time 15 with a pause, ends mid word",
+ "recordedWith": "web",
+ "config": {
+ "mode": "time",
+ "time": 15,
+ "words": 50,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 176,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 145,
+ "hold": 48
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 182,
+ "hold": 93
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 148,
+ "hold": 58
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 122,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 113,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": "v",
+ "gap": 176,
+ "hold": 92
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 185,
+ "hold": 137
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 129,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 124,
+ "hold": 114
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 153,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 155,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": "v",
+ "gap": 195,
+ "hold": 74
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 202,
+ "hold": 125
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 140,
+ "hold": 119
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 109,
+ "hold": 80
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 177,
+ "hold": 40
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 91,
+ "hold": 93
+ },
+ {
+ "kind": "key",
+ "key": "k",
+ "gap": 194,
+ "hold": 106
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 172,
+ "hold": 126
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 160,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 183,
+ "hold": 134
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 160,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 99,
+ "hold": 81
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 154,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 140,
+ "hold": 91
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 150,
+ "hold": 103
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 170,
+ "hold": 81
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 173,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 146,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 196,
+ "hold": 118
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 171,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 182,
+ "hold": 46
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 98,
+ "hold": 99
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 194,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 155,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 146,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 159,
+ "hold": 83
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 181,
+ "hold": 59
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 204,
+ "hold": 111
+ },
+ {
+ "kind": "pause",
+ "ms": 1600
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 180,
+ "hold": 125
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 97,
+ "hold": 85
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 109,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 183,
+ "hold": 108
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 208,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 208,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 196,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 188,
+ "hold": 75
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 165,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 137,
+ "hold": 106
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 107,
+ "hold": 109
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 156,
+ "hold": 68
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 196,
+ "hold": 45
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 192,
+ "hold": 50
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 141,
+ "hold": 138
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 93,
+ "hold": 114
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 102,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 209,
+ "hold": 98
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 112,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 109,
+ "hold": 111
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 166,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 173,
+ "hold": 129
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 105,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 105,
+ "hold": 107
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 171,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 177,
+ "hold": 75
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 204,
+ "hold": 50
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 206,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "k",
+ "gap": 136,
+ "hold": 95
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 142,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 123,
+ "hold": 114
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 91,
+ "hold": 91
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 184,
+ "hold": 77
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 144,
+ "hold": 81
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 209,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 102,
+ "hold": 60
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 105,
+ "hold": 135
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 183,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 136,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 130,
+ "hold": 69
+ },
+ {
+ "kind": "key",
+ "key": "g",
+ "gap": 106,
+ "hold": 54
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 139,
+ "hold": 65
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 92,
+ "hold": 100
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -6.5,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "i"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473705135
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 53.1,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 135.2,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 136.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "in"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 183.6,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 319.1,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 321,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "int"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 411.9,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 466.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 469,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "into"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 525.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 588.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 591.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 4,
+ "inputValue": "into ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 689.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 815.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 817.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 864.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 992.3,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 994.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "v",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "ov"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1007.4,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 7.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1082.7,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1176.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1178.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "ove"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1305.5,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1307.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "over"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1310.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1421.3,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1427,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1428.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": "over ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1543.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1714.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1716.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "h"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1846.6,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1868.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1870.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "ha"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1909.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2006.6,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 6.6
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2065.4,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2067.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "v",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "hav"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2138.7,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2265.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2267.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "have"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2392.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2404.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2406.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "have ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2526.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2647.3,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2650.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "l"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2728.3,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2826.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2828.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "lo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2863.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2914.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2915.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "loo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3006.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3007.1,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 7.1
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3107.1,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3107.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "k",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "look"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3213.3,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3280.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3282.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "look ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3406.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3579,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3580.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "h"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3682,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3761.1,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3762.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "hp"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3895.4,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3919.1,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3920.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "hph"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4007.1,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 7.1
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4020.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4022.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "hphe"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4026.7,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4101.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4175.5,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4179.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "hpher"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4307.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4312.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4314.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 5,
+ "inputValue": "hphere"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4405,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4465.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4466.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 6,
+ "inputValue": "hphere ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4567.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4752.2,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4754.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "n"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4832.2,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4923.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4925.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "no"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5007,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5042.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5074.2,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5075.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "not"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5219,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5265,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5267,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "not ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5384.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5564.7,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5566.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5710.2,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5747.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5749.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "me"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5793.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5845.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5847.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "mea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5944.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6005.1,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 5.1
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6038,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6038.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 3,
+ "inputValue": "mean"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6114.6,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6195.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6197.3,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 4,
+ "inputValue": "mean ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6266,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6422.5,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6424.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "f"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6526.8,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6582.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6584.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "fo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6665.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6763.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6766.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "for"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6822.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6967.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6969.3,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "for ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7007.1,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 7.1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7078.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8007.2,
+ "data": {
+ "event": "step",
+ "timer": 8,
+ "drift": 7.2
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8868.8,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8871.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8967,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8969,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "mo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8993.3,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 9005.6,
+ "data": {
+ "event": "step",
+ "timer": 9,
+ "drift": 5.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9050.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9075.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9076.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "mos"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9193.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9259.3,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9261.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 3,
+ "inputValue": "most"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9365.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9465.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9466.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 4,
+ "inputValue": "most ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9615.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9837.2,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9839.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9942.7,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 10006.5,
+ "data": {
+ "event": "step",
+ "timer": 10,
+ "drift": 6.5
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10032.2,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10033.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "wa"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10178.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10221,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10223.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "wan"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10295.5,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10385.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10387.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "want"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10522.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10524.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 4,
+ "inputValue": "want ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10530.3,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10627.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10744.7,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10746.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 0,
+ "inputValue": "l"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10853.1,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10898.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10900.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 1,
+ "inputValue": "le"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10969,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 11007.3,
+ "data": {
+ "event": "step",
+ "timer": 11,
+ "drift": 7.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11096.7,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11098.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 2,
+ "inputValue": "lea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11143.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11288.8,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11290.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 3,
+ "inputValue": "lead"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11338.5,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11429.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11431.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 10,
+ "charIndex": 4,
+ "inputValue": "lead ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11567.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11671.6,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11674,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11775.9,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11778.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 1,
+ "inputValue": "mu"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11783.1,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 11844.7,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 11981.1,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 11982.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 2,
+ "inputValue": "muc"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 12007,
+ "data": {
+ "event": "step",
+ "timer": 12,
+ "drift": 7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12081,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12094.7,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12096.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 3,
+ "inputValue": "much"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12135.1,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12204.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12206.9,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 11,
+ "charIndex": 4,
+ "inputValue": "much ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12315,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12493.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12495.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12623.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12664.6,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12666.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 1,
+ "inputValue": "of"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12767.9,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12769.9,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12771.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 2,
+ "inputValue": "off"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12791.8,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 12874.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 12876.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 12,
+ "charIndex": 3,
+ "inputValue": "off ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 12980.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 13007.1,
+ "data": {
+ "event": "step",
+ "timer": 13,
+ "drift": 7.1
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 13162.3,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 13164.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 13266.6,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 13339.3,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 13341.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 1,
+ "inputValue": "th"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 13414.6,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 13543.3,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 13545.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 2,
+ "inputValue": "thi"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 13593.1,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 13748.7,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 13750.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 3,
+ "inputValue": "thin"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 13808.7,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 13885.3,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 13886.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "k",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 4,
+ "inputValue": "think"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 13980,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 14007.3,
+ "data": {
+ "event": "step",
+ "timer": 14,
+ "drift": 7.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14026.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14028.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 13,
+ "charIndex": 5,
+ "inputValue": "think ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14127.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14261.8,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14264,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 14,
+ "charIndex": 0,
+ "inputValue": "y"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14352.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14354.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 14,
+ "charIndex": 1,
+ "inputValue": "ye"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14373.2,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14442.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14536.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14538.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 14,
+ "charIndex": 2,
+ "inputValue": "yea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14611.3,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14680,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14682.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 14,
+ "charIndex": 3,
+ "inputValue": "year"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14760.7,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 14888.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 14891.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 14,
+ "charIndex": 4,
+ "inputValue": "year ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 14937.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 15006,
+ "data": {
+ "event": "step",
+ "timer": 15,
+ "drift": 6
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 15007,
+ "data": {
+ "event": "end",
+ "timer": 15,
+ "date": 1791473720140
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "into ",
+ "over ",
+ "have ",
+ "look ",
+ "here ",
+ "not ",
+ "mean ",
+ "for ",
+ "most ",
+ "want ",
+ "lead ",
+ "much ",
+ "off ",
+ "think ",
+ "year ",
+ "through ",
+ "to ",
+ "off ",
+ "head ",
+ "here ",
+ "also ",
+ "life ",
+ "end ",
+ "make ",
+ "would ",
+ "real ",
+ "first ",
+ "know ",
+ "into ",
+ "give ",
+ "general ",
+ "help ",
+ "own ",
+ "state ",
+ "on ",
+ "follow ",
+ "fact ",
+ "which ",
+ "develop ",
+ "must ",
+ "about ",
+ "keep ",
+ "but ",
+ "down ",
+ "against ",
+ "make ",
+ "since ",
+ "time ",
+ "begin ",
+ "house ",
+ "after ",
+ "so ",
+ "call ",
+ "child ",
+ "change ",
+ "word ",
+ "where ",
+ "get ",
+ "up ",
+ "day ",
+ "point ",
+ "must ",
+ "help ",
+ "through ",
+ "out ",
+ "he ",
+ "mean ",
+ "fact ",
+ "public ",
+ "just ",
+ "these ",
+ "during ",
+ "develop ",
+ "down ",
+ "ask ",
+ "general ",
+ "small ",
+ "great ",
+ "both ",
+ "order ",
+ "do ",
+ "man ",
+ "come ",
+ "increase ",
+ "show ",
+ "the ",
+ "down ",
+ "one ",
+ "do ",
+ "change ",
+ "with ",
+ "the ",
+ "right ",
+ "can ",
+ "down ",
+ "present ",
+ "new ",
+ "seem ",
+ "under ",
+ "you ",
+ "any ",
+ "leave ",
+ "large ",
+ "line ",
+ "under ",
+ "interest ",
+ "child ",
+ "know ",
+ "way ",
+ "general ",
+ "set ",
+ "old ",
+ "become ",
+ "interest ",
+ "last "
+ ],
+ "mode": "time",
+ "mode2": "15",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 54.36,
+ "rawWpm": 59.96,
+ "charStats": [68, 3, 2, 0],
+ "charTotal": 75,
+ "acc": 93.33,
+ "language": "english",
+ "testDuration": 15.01,
+ "lastKeyToEnd": 118.4,
+ "startToFirstKey": 0,
+ "afkDuration": 1,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473720263,
+ "mode": "time",
+ "mode2": "15",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 64.99,
+ "wpmConsistency": 81.89,
+ "keyConsistency": 7.22,
+ "chartData": {
+ "wpm": [84, 72, 72, 60, 53, 54, 57, 50, 47, 47, 49, 51, 53, 53, 54],
+ "burst": [84, 60, 72, 60, 72, 60, 72, 0, 24, 48, 72, 72, 72, 60, 72],
+ "err": [0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 135.2, 183.90000000000003, 147.79999999999995, 121.30000000000007,
+ 227.29999999999995, 176.79999999999995, 184.29999999999995,
+ 128.9000000000001, 121.5, 287.5, 154.29999999999995, 196.60000000000014,
+ 200.4000000000001, 139.0999999999999, 242.4000000000001, 179,
+ 87.89999999999964, 192.9000000000001, 173.5999999999999,
+ 298.3000000000002, 182.0999999999999, 158, 101.5, 154.9000000000001,
+ 137.10000000000036, 152.59999999999945, 287, 171.69999999999982,
+ 150.30000000000018, 190.80000000000018, 299.6999999999998,
+ 182.40000000000055, 98.5, 192.39999999999964, 157.19999999999982,
+ 227.30000000000018, 159.80000000000018, 181.5, 203.39999999999964,
+ 1901.5999999999995, 98.20000000000073, 108.60000000000036,
+ 183.6999999999989, 205.90000000000146, 372, 195, 188.79999999999927,
+ 164.79999999999927, 136.5, 222.40000000000146, 153.89999999999964,
+ 198.10000000000036, 192.09999999999854, 140.90000000000146,
+ 241.89999999999964, 104.29999999999927, 205.20000000000073,
+ 113.60000000000036, 109.89999999999964, 288.5, 171.5, 105.29999999999927,
+ 104.30000000000109, 288.09999999999854, 177, 204, 205.40000000000146,
+ 136.59999999999854, 141.20000000000073, 235.29999999999927,
+ 90.30000000000109, 184.5, 143.39999999999964, 208.60000000000036
+ ],
+ "keyDuration": [
+ 59.6, 48.400000000000006, 92.79999999999995, 58.39999999999998,
+ 100.89999999999998, 48.700000000000045, 90.40000000000009,
+ 134.20000000000005, 115.79999999999995, 116.5, 132.0999999999999,
+ 40.299999999999955, 73.29999999999973, 126.39999999999964,
+ 121.19999999999982, 81, 37, 92.60000000000036, 106.20000000000027,
+ 125.40000000000009, 103, 134.30000000000018, 107.59999999999991,
+ 80.90000000000009, 132.30000000000018, 92.39999999999964,
+ 102.40000000000055, 80, 118.90000000000055, 144.80000000000018,
+ 119.30000000000018, 145.5, 46.099999999999454, 98.5, 76.60000000000036,
+ 70.80000000000018, 104.30000000000018, 82.80000000000018,
+ 58.30000000000018, 111.10000000000036, 124.5, 83.5, 117.89999999999964,
+ 106.5, 150, 105.5, 146.59999999999854, 74.5, 144.5, 105.5,
+ 108.39999999999964, 70.39999999999964, 46.399999999999636,
+ 49.70000000000073, 137.39999999999964, 111.5, 68.80000000000109,
+ 99.89999999999964, 40.399999999999636, 110.39999999999964,
+ 130.10000000000036, 103.29999999999927, 21.899999999999636,
+ 106.09999999999854, 104.30000000000109, 75.30000000000109,
+ 49.80000000000109, 60, 94.70000000000073, 100.89999999999964,
+ 111.40000000000146, 90.10000000000036, 74.69999999999891,
+ 80.70000000000073, 49
+ ],
+ "keyOverlap": 74,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/words-10-clean.json b/packages/typing-fixtures/reference/keystrokes/words-10-clean.json
new file mode 100644
index 000000000..ce62aaa09
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/words-10-clean.json
@@ -0,0 +1,1560 @@
+{
+ "name": "words-10-clean",
+ "description": "words 10, no mistakes, overlapping keys",
+ "recordedWith": "web",
+ "config": {
+ "mode": "words",
+ "time": 30,
+ "words": 10,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 98,
+ "hold": 40
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 92,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 118,
+ "hold": 71
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 97,
+ "hold": 119
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 86,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 87,
+ "hold": 94
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 68,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 75,
+ "hold": 57
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 89,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 84,
+ "hold": 124
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 77,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 63,
+ "hold": 87
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 95,
+ "hold": 128
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 78,
+ "hold": 63
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 99,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 106,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 103,
+ "hold": 90
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 106,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 74,
+ "hold": 123
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 69,
+ "hold": 76
+ },
+ {
+ "kind": "key",
+ "key": "v",
+ "gap": 85,
+ "hold": 67
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 69,
+ "hold": 120
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 95,
+ "hold": 54
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 76,
+ "hold": 45
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 97,
+ "hold": 135
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 74,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 103,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 78,
+ "hold": 114
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 117,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 102,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 110,
+ "hold": 143
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 67,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 112,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 113,
+ "hold": 96
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 116,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 70,
+ "hold": 114
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 117,
+ "hold": 55
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 119,
+ "hold": 58
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 83,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 84,
+ "hold": 76
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 99,
+ "hold": 96
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 92,
+ "hold": 48
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 108,
+ "hold": 134
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -4.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473675188
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 32.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 87.5,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 88.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "of"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 200.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 202.9,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "of ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 231.3,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 272.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 374.5,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 375.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "b"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 461.5,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 462.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "by"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 494.2,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 548.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 550.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "by ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 611.2,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 641.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 714.8,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 715.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "c"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 790.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 791.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "co"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 799.3,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 847,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 879.2,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 880.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "com"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 925.8,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 963.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 965,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "come"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1003.3,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 3.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1041.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1043.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "come ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1088.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1101.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1173.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1174.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "a"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1260.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1267,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1267.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "as"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1347.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1348.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "as ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1396.9,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1408.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1516.7,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1518.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "c"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1616.5,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1619.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1620.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "ca"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1682,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1724.8,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1726.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "can"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1812.6,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1830.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1831.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "can ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1980.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2003,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2067.4,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2069.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "n"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2134.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2136.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "ne"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2190.3,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2210.4,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2217.5,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2218.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "v",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "nev"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2286.6,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2290.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2291.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "neve"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2383.6,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2385.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 4,
+ "inputValue": "never"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2408.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2438.6,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2459.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2460.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 5,
+ "inputValue": "never ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2504.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2613.2,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2615.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2686.8,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2688.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "th"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2747.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2787.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2788.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "the"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2837.8,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2866.3,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2866.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 3,
+ "inputValue": "they"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2870.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2980.4,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2981.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2982.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 4,
+ "inputValue": "they ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3002.9,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 2.9
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3126.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3240.7,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3243,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "b"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3351.1,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3353.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "by"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3379,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3418.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3420.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "by ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3460.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3508.5,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3634.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3636.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3706.9,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3748,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3750.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "sy"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3844,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3865.4,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3867.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "sys"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3932.6,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3933.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 3,
+ "inputValue": "syst"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3980.3,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4003.1,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 3.1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4047.6,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4050.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4051.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 4,
+ "inputValue": "syste"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4106.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4170.1,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4172.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 5,
+ "inputValue": "system"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4226.2,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4252.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4254.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 6,
+ "inputValue": "system ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4304.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4397.5,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4399.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "m",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4471.9,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4496.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4498.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "mo",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4588.7,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4590.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "mos",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4592.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4635.1,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4695.1,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4697.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "most",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4698.4,
+ "data": {
+ "event": "end",
+ "timer": 4,
+ "date": 1791473679885
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4799.83,
+ "data": {
+ "code": "KeyT",
+ "estimated": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "of ",
+ "by ",
+ "come ",
+ "as ",
+ "can ",
+ "never ",
+ "they ",
+ "by ",
+ "system ",
+ "most"
+ ],
+ "mode": "words",
+ "mode2": "10",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 109.79,
+ "rawWpm": 109.79,
+ "charStats": [43, 0, 0, 0],
+ "charTotal": 43,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 4.7,
+ "lastKeyToEnd": 3.3,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473680017,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 84.03,
+ "wpmConsistency": 95.73,
+ "keyConsistency": 59.8,
+ "chartData": {
+ "wpm": [120, 108, 116, 108, 110],
+ "burst": [120, 96, 132, 84, 120],
+ "err": [0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 87.5, 113.30000000000001, 173.7, 87, 87.39999999999998,
+ 165.89999999999998, 75.70000000000005, 88.70000000000005,
+ 84.39999999999998, 77.60000000000002, 132.5999999999999,
+ 93.20000000000005, 80.29999999999995, 169.4000000000001,
+ 102.79999999999995, 105.29999999999995, 105.40000000000009,
+ 237.20000000000005, 67.19999999999982, 82.90000000000009,
+ 72.69999999999982, 93.40000000000009, 75.59999999999991, 154,
+ 73.60000000000036, 100.89999999999964, 78.60000000000036,
+ 115.29999999999973, 259.0999999999999, 110.40000000000009,
+ 67.09999999999991, 216.60000000000036, 113.19999999999982,
+ 117.40000000000009, 67.19999999999982, 117.70000000000027,
+ 119.80000000000018, 82.5, 144.89999999999964, 99.19999999999982, 92,
+ 106.40000000000055
+ ],
+ "keyDuration": [
+ 37, 143.8, 71.69999999999999, 119.69999999999999, 149.70000000000005,
+ 92.80000000000007, 84.5, 56.5, 46.59999999999991, 124.89999999999998,
+ 60.09999999999991, 86.70000000000005, 129.9000000000001,
+ 61.299999999999955, 99.79999999999995, 62.5, 87.79999999999995,
+ 150.0999999999999, 122.90000000000009, 75.80000000000018,
+ 69.09999999999991, 118.5, 55, 45.5, 134.60000000000036, 151,
+ 83.10000000000036, 114.09999999999991, 145.20000000000027,
+ 138.30000000000018, 157.4000000000001, 42.70000000000027,
+ 72.09999999999991, 96, 114.90000000000009, 115, 56.399999999999636,
+ 56.099999999999454, 51.5, 74.39999999999964, 95.5, 46.400000000000546,
+ 104.72999999999956
+ ],
+ "keyOverlap": 549.4,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/words-10-errors.json b/packages/typing-fixtures/reference/keystrokes/words-10-errors.json
new file mode 100644
index 000000000..b37157a37
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/words-10-errors.json
@@ -0,0 +1,2152 @@
+{
+ "name": "words-10-errors",
+ "description": "words 10 with a corrected typo, extra letters, a skipped word, word delete and a backtrack",
+ "recordedWith": "web",
+ "config": {
+ "mode": "words",
+ "time": 30,
+ "words": 10,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 178,
+ "hold": 76
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 124,
+ "hold": 99
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 195,
+ "hold": 109
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 150,
+ "hold": 79
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 184,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": "q",
+ "gap": 184,
+ "hold": 62
+ },
+ {
+ "kind": "key",
+ "key": "Backspace",
+ "gap": 141,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 160,
+ "hold": 56
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 171,
+ "hold": 80
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 207,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 200,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 105,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 91,
+ "hold": 143
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 144,
+ "hold": 110
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 209,
+ "hold": 66
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 146,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 96,
+ "hold": 97
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 101,
+ "hold": 46
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 205,
+ "hold": 129
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 90,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 99,
+ "hold": 147
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 153,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": "x",
+ "gap": 171,
+ "hold": 46
+ },
+ {
+ "kind": "key",
+ "key": "x",
+ "gap": 183,
+ "hold": 140
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 189,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 204,
+ "hold": 48
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 176,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 191,
+ "hold": 111
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 153,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 192,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 156,
+ "hold": 74
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 198,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 144,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": "Alt+Backspace",
+ "gap": 92,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 128,
+ "hold": 88
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 186,
+ "hold": 135
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 200,
+ "hold": 87
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 115,
+ "hold": 111
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 165,
+ "hold": 118
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 203,
+ "hold": 63
+ },
+ {
+ "kind": "key",
+ "key": "Backspace",
+ "gap": 123,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 154,
+ "hold": 43
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 125,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 110,
+ "hold": 125
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 137,
+ "hold": 86
+ },
+ {
+ "kind": "key",
+ "key": "v",
+ "gap": 137,
+ "hold": 133
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 147,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 121,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 175,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 178,
+ "hold": 126
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 168,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 129,
+ "hold": 112
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 103,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 178,
+ "hold": 147
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 166,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": "g",
+ "gap": 159,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 93,
+ "hold": 104
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 152,
+ "hold": 119
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 192,
+ "hold": 87
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 151,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 194,
+ "hold": 89
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -5.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "a"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473691137
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 66.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 115,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 116,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "al"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 214.4,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 308.7,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 309.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "all"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 419.7,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 460.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 462.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "all ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 540.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 737.7,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 740.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 789.9,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 919.9,
+ "data": {
+ "code": "KeyQ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 922.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "q",
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "mq"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 981.4,
+ "data": {
+ "code": "KeyQ"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1004.8,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 4.8
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1064.3,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1220.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1223.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "ma"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1277.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1391.9,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1394.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "man"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1470.3,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1598.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1601.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "man ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1642.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1850,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1851.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1896,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1956.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1958.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "wo"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2004.4,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 4.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2016.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2047.9,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2049.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "wou"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2191,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2193.4,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2195.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "woul"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2301.1,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2400.7,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2403.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "would"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2466.9,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2546.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2549.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 5,
+ "inputValue": "would ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2623.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2730.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2733.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2826.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2828.1,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2829.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "sc"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2876.3,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3004.4,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 4.4
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3035.6,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3038,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "sch"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3126,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3127.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "scho"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3163.9,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3225.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3228.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3229.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "schoo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3274,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3373.9,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3375,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 5,
+ "inputValue": "school"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3438.6,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3549.8,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3554.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "x",
+ "correct": false,
+ "wordIndex": 3,
+ "charIndex": 6,
+ "inputValue": "schoolx"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3594.6,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3732.7,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3736.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "x",
+ "correct": false,
+ "wordIndex": 3,
+ "charIndex": 7,
+ "inputValue": "schoolxx"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3873.6,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3921.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3923.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": false,
+ "wordIndex": 3,
+ "charIndex": 8,
+ "inputValue": "schoolxx ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4004.6,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 4.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4009.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4225.1,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4227.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4273.2,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4400.7,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4402.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "wh"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4548.6,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4592.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4594.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "whe"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4700.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4743.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4746.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "wher"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4788.5,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4936.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4938.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "where"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5004.6,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 4.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5067.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5094.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5095.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 5,
+ "inputValue": "where ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5165.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5372.2,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5374.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "d"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5514.3,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5516.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5518.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "da"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5593.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5606.5,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": ""
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5766.4,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5768.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "d"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5855.5,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5952.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5954.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "da"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6004.6,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 4.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6087.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6153,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6155.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "day"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6239.3,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6266.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6267.5,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "day ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6377.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6550.7,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6553.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "d"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6669.3,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6753.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6755.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": false,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "d ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6817.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6880.7,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "d"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7004.7,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 4.7
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7030.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7032.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "do"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7073.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7156.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7158.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "do ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7286.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7407,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7409.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "n"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7531.8,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7540.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7542,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "ne"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7629.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7679.3,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7680.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "v",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "nev"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7813.2,
+ "data": {
+ "code": "KeyV"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7826.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7828.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "neve"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7948.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7949.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 4,
+ "inputValue": "never"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7975.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7996.2,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8004.4,
+ "data": {
+ "event": "step",
+ "timer": 8,
+ "drift": 4.4
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8122.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8125.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 5,
+ "inputValue": "never ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8165.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8356.4,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8358.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8480.6,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8522,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8523.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "we"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8593.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8650.9,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8652.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "wel"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8753.4,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8755,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8755.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 3,
+ "inputValue": "well"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8759.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8931.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8932.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 4,
+ "inputValue": "well ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 9004.7,
+ "data": {
+ "event": "step",
+ "timer": 9,
+ "drift": 4.7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9079.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9257.3,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9259.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "a",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9308.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9416.3,
+ "data": {
+ "code": "KeyG"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9418.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "g",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "ag",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9509.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9510.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "aga",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9559.5,
+ "data": {
+ "code": "KeyG"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9612.9,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9661.7,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9664.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "agai",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9779.3,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9853.8,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9856.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 4,
+ "inputValue": "again",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9939.4,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10004.3,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10006.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 5,
+ "inputValue": "agains",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 10007.4,
+ "data": {
+ "event": "step",
+ "timer": 10,
+ "drift": 7.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10051.3,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 10199.3,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10201.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 6,
+ "inputValue": "against",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 10203,
+ "data": {
+ "event": "end",
+ "timer": 10,
+ "date": 1791473701338
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10288.8,
+ "data": {
+ "code": "KeyT"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "all ",
+ "man ",
+ "would ",
+ "school ",
+ "where ",
+ "day ",
+ "do ",
+ "never ",
+ "well ",
+ "against"
+ ],
+ "mode": "words",
+ "mode2": "10",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 52.94,
+ "rawWpm": 63.53,
+ "charStats": [45, 1, 2, 0],
+ "charTotal": 54,
+ "acc": 91.38,
+ "language": "english",
+ "testDuration": 10.2,
+ "lastKeyToEnd": 3.7,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473701467,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 83.63,
+ "wpmConsistency": 86.18,
+ "keyConsistency": 67.27,
+ "chartData": {
+ "wpm": [48, 60, 64, 42, 46, 44, 43, 48, 51, 52],
+ "burst": [72, 60, 72, 84, 60, 60, 48, 84, 72, 60],
+ "err": [1, 0, 0, 3, 0, 0, 1, 0, 0, 0]
+ },
+ "keySpacing": [
+ 115, 193.7, 151.60000000000002, 277.40000000000003, 182.19999999999993,
+ 300.6, 171.4000000000001, 206.89999999999986, 251.20000000000005,
+ 106.79999999999995, 91.10000000000014, 145.5, 207.29999999999973,
+ 146.20000000000027, 183.9000000000001, 97.29999999999973, 207.5,
+ 90.40000000000009, 102.09999999999991, 145.80000000000018,
+ 175.9000000000001, 182.89999999999964, 189.20000000000027,
+ 303.2000000000003, 175.59999999999945, 191.60000000000036,
+ 151.59999999999945, 192.90000000000055, 157.39999999999964, 278,
+ 144.40000000000055, 249.79999999999927, 186.20000000000073,
+ 200.39999999999964, 113.10000000000036, 284.59999999999945,
+ 202.69999999999982, 277.5, 125.80000000000018, 250.30000000000018,
+ 133.89999999999964, 138.40000000000055, 146.89999999999964,
+ 121.90000000000055, 174.79999999999927, 233.5, 165.60000000000036,
+ 128.89999999999964, 104.10000000000036, 176.39999999999964,
+ 325.89999999999964, 159, 93.30000000000109, 152.10000000000036,
+ 192.09999999999854, 150.5, 195
+ ],
+ "keyDuration": [
+ 72.4, 99.4, 111, 80.40000000000003, 52.19999999999993, 61.5,
+ 57.09999999999991, 78.39999999999986, 43.700000000000045, 46,
+ 59.90000000000009, 143.0999999999999, 107.69999999999982,
+ 66.20000000000027, 76.40000000000009, 95.69999999999982,
+ 48.20000000000027, 128.30000000000018, 99.5, 45.90000000000009,
+ 64.69999999999982, 44.79999999999973, 140.9000000000001,
+ 87.69999999999982, 48.099999999999454, 147.90000000000055,
+ 108.59999999999945, 44.600000000000364, 131, 71.19999999999982,
+ 142.10000000000036, 77.19999999999982, 89.10000000000036,
+ 134.89999999999964, 86.30000000000018, 111, 118.60000000000036,
+ 63.80000000000018, 43, 129.90000000000055, 124.80000000000018,
+ 88.40000000000055, 133.89999999999964, 149.60000000000036,
+ 48.099999999999454, 42.400000000000546, 124.20000000000073, 71.5, 102.5,
+ 4.799999999999272, 148.5, 51.600000000000364, 143.20000000000073,
+ 103.29999999999927, 117.59999999999854, 85.60000000000036, 47, 89.5
+ ],
+ "keyOverlap": 115.5,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/words-10-punctuation-numbers.json b/packages/typing-fixtures/reference/keystrokes/words-10-punctuation-numbers.json
new file mode 100644
index 000000000..2e6775b88
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/words-10-punctuation-numbers.json
@@ -0,0 +1,1855 @@
+{
+ "name": "words-10-punctuation-numbers",
+ "description": "words 10 with punctuation and numbers",
+ "recordedWith": "web",
+ "config": {
+ "mode": "words",
+ "time": 30,
+ "words": 10,
+ "language": "english",
+ "punctuation": true,
+ "numbers": true,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "2",
+ "gap": 109,
+ "hold": 109
+ },
+ {
+ "kind": "key",
+ "key": "5",
+ "gap": 126,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 160,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 188,
+ "hold": 66
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 151,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 152,
+ "hold": 48
+ },
+ {
+ "kind": "key",
+ "key": ".",
+ "gap": 158,
+ "hold": 64
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 109,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "B",
+ "gap": 173,
+ "hold": 50
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 105,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "f",
+ "gap": 157,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 180,
+ "hold": 125
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 95,
+ "hold": 90
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 133,
+ "hold": 95
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 129,
+ "hold": 97
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 105,
+ "hold": 135
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 129,
+ "hold": 121
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 198,
+ "hold": 96
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 172,
+ "hold": 123
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 168,
+ "hold": 74
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 123,
+ "hold": 51
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 127,
+ "hold": 150
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 158,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 96,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 185,
+ "hold": 150
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 120,
+ "hold": 135
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 99,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 154,
+ "hold": 60
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 139,
+ "hold": 53
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 103,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 191,
+ "hold": 61
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 93,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "3",
+ "gap": 163,
+ "hold": 127
+ },
+ {
+ "kind": "key",
+ "key": "4",
+ "gap": 138,
+ "hold": 89
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 94,
+ "hold": 86
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 126,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 90,
+ "hold": 123
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 135,
+ "hold": 68
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 208,
+ "hold": 138
+ },
+ {
+ "kind": "key",
+ "key": "y",
+ "gap": 191,
+ "hold": 105
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 170,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 127,
+ "hold": 50
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 160,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 130,
+ "hold": 138
+ },
+ {
+ "kind": "key",
+ "key": ",",
+ "gap": 129,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 124,
+ "hold": 120
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 154,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 179,
+ "hold": 97
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 176,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 134,
+ "hold": 69
+ },
+ {
+ "kind": "key",
+ "key": ".",
+ "gap": 207,
+ "hold": 47
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -6.6,
+ "data": {
+ "code": "Digit2"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "2",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "2"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473772708
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 98.8,
+ "data": {
+ "code": "Digit2"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 117.1,
+ "data": {
+ "code": "Digit5"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 118,
+ "data": {
+ "inputType": "insertText",
+ "data": "5",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "25"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 232.9,
+ "data": {
+ "code": "Digit5"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 276.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 278.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "25 ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 366,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 565.6,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 568.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "f"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 630.7,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 716.4,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 718.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "fo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 762.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 868.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 869.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "for"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 915.1,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1001.3,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 1.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1025.5,
+ "data": {
+ "code": "Period"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1027.4,
+ "data": {
+ "inputType": "insertText",
+ "data": ".",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "for."
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1088.8,
+ "data": {
+ "code": "Period"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1130.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1131.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": "for. ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1279.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1461.6,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1464.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "B",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "B"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1511.8,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1565.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1566.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "Be"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1609.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1725.2,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1727.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "f",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "Bef"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1813,
+ "data": {
+ "code": "KeyF"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1904.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1907.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "Befo"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1998.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1999.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "Befor"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2000.8,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 0.8
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2027.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2088.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2132.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2134.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 5,
+ "inputValue": "Before"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2227.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2261.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2263.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 6,
+ "inputValue": "Before ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2356.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2470.2,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2472.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2598,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2599.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "tu"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2602.1,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2719,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2796.3,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2798.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "tur"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2891.2,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2968.8,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2970.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "turn"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3000.1,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 0.1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3089.8,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3133.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3134.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "turn ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3209.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3338.1,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3339.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3389.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3464.7,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3466.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "sc"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3616.4,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3622.3,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3624.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "sch"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3707.1,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3718.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3719.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "scho"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3808.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3905.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3907.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "schoo"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4001.3,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 1.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4025.1,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4026,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 5,
+ "inputValue": "school"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4053.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4123.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4124.6,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 6,
+ "inputValue": "school ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4159.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4224.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4386.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4389.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "l"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4446.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4526.5,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4528.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "li"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4578.6,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4628.3,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4629.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "lin"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4778.5,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4820,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4822,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "line"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4880.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4913.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4915.5,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 4,
+ "inputValue": "line ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000.9,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 0.9
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5056.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5229.8,
+ "data": {
+ "code": "Digit3"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5232,
+ "data": {
+ "inputType": "insertText",
+ "data": "3",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "3"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5356.2,
+ "data": {
+ "code": "Digit3"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5366.1,
+ "data": {
+ "code": "Digit4"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5367.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "4",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "34"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5456.2,
+ "data": {
+ "code": "Digit4"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5457.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5458.8,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "34 ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5547.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5682.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5685,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "e"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5773.6,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5774.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "ea"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5799,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5896.3,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5907,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5908.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "ear"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5977.8,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6001.2,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 1.2
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6116.4,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6118.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "earl"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6254.6,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6307.5,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6309.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "y",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 4,
+ "inputValue": "early"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6412.3,
+ "data": {
+ "code": "KeyY"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6477.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6480.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 5,
+ "inputValue": "early ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6624.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6762.1,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6764.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "n"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6811.1,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6921.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6924.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "no"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7001.1,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 1.1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7005.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7051.7,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7053.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "not"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7180.6,
+ "data": {
+ "code": "Comma"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7182.7,
+ "data": {
+ "inputType": "insertText",
+ "data": ",",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 3,
+ "inputValue": "not,"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7186.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7279.4,
+ "data": {
+ "code": "Comma"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7304.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7306.5,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 4,
+ "inputValue": "not, ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7424.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7586.9,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7589.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "m",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7671,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7764.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7765.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "mo",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7861.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7941.2,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7942.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "mos",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8001,
+ "data": {
+ "event": "step",
+ "timer": 8,
+ "drift": 1
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8073.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8075.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8077.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "most",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8146,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8283.8,
+ "data": {
+ "code": "Period"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8286.1,
+ "data": {
+ "inputType": "insertText",
+ "data": ".",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 4,
+ "inputValue": "most.",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8287.6,
+ "data": {
+ "event": "end",
+ "timer": 8,
+ "date": 1791473780994
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8330.1,
+ "data": {
+ "code": "Period"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "25 ",
+ "for. ",
+ "Before ",
+ "turn ",
+ "school ",
+ "line ",
+ "34 ",
+ "early ",
+ "not, ",
+ "most."
+ ],
+ "mode": "words",
+ "mode2": "10",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 73.82,
+ "rawWpm": 73.82,
+ "charStats": [51, 0, 0, 0],
+ "charTotal": 51,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 8.29,
+ "lastKeyToEnd": 3.8,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": true,
+ "numbers": true,
+ "lazyMode": false,
+ "timestamp": 1791473781136,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 90.21,
+ "wpmConsistency": 97.63,
+ "keyConsistency": 63.72,
+ "chartData": {
+ "wpm": [72, 78, 76, 75, 77, 76, 74, 74],
+ "burst": [72, 84, 72, 72, 84, 72, 60, 72],
+ "err": [0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 117.1, 159.6, 288.90000000000003, 150.79999999999995, 151.70000000000005,
+ 157.39999999999998, 104.90000000000009, 331.1999999999998, 104,
+ 159.60000000000014, 179.5, 94.09999999999991, 133.50000000000023,
+ 129.39999999999964, 208.5, 127.80000000000018, 198.30000000000018, 172.5,
+ 165, 204.29999999999973, 126.59999999999991, 157.60000000000036,
+ 96.19999999999982, 186.80000000000018, 119.79999999999973,
+ 98.59999999999991, 263.10000000000036, 139.69999999999982,
+ 101.80000000000018, 191.69999999999982, 93.10000000000036,
+ 316.6999999999998, 136.30000000000018, 91.59999999999945, 225,
+ 90.90000000000055, 133.39999999999964, 209.39999999999964,
+ 191.10000000000036, 170.39999999999964, 284.2000000000007,
+ 159.79999999999927, 129.80000000000018, 128.90000000000055,
+ 124.09999999999945, 282.1999999999998, 178, 176.30000000000018,
+ 134.60000000000036, 207.9999999999991
+ ],
+ "keyDuration": [
+ 105.39999999999999, 115.80000000000001, 89.30000000000001,
+ 65.10000000000002, 46.39999999999998, 47, 63.299999999999955, 149.5,
+ 50.200000000000045, 44, 87.79999999999995, 123, 90.10000000000014, 95.5,
+ 94.70000000000027, 131.9000000000001, 121, 94.89999999999964, 121,
+ 75.29999999999973, 51.5, 151.70000000000027, 84.79999999999973,
+ 90.40000000000009, 147.89999999999964, 134.70000000000027, 100.5, 60,
+ 52.100000000000364, 150.19999999999982, 60.899999999999636, 143.5,
+ 126.39999999999964, 90.09999999999945, 89.5, 116.30000000000018,
+ 122.69999999999982, 70.80000000000018, 138.20000000000073,
+ 104.80000000000018, 146.30000000000018, 49, 83.20000000000073,
+ 135.10000000000036, 98.79999999999927, 119.60000000000036,
+ 84.10000000000036, 96.80000000000018, 132.60000000000036,
+ 70.19999999999982, 46.30000000000109
+ ],
+ "keyOverlap": 128.8,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/words-10-stop-on-letter.json b/packages/typing-fixtures/reference/keystrokes/words-10-stop-on-letter.json
new file mode 100644
index 000000000..a5583f71b
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/words-10-stop-on-letter.json
@@ -0,0 +1,2168 @@
+{
+ "name": "words-10-stop-on-letter",
+ "description": "stop on error letter, blocked wrong letters",
+ "recordedWith": "web",
+ "config": {
+ "mode": "words",
+ "time": 30,
+ "words": 10,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "letter",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 91,
+ "hold": 47
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 207,
+ "hold": 117
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 153,
+ "hold": 85
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 146,
+ "hold": 66
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 156,
+ "hold": 120
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 121,
+ "hold": 57
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 182,
+ "hold": 97
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 114,
+ "hold": 80
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 125,
+ "hold": 99
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 126,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 120,
+ "hold": 137
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 113,
+ "hold": 56
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 109,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 156,
+ "hold": 124
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 99,
+ "hold": 140
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 156,
+ "hold": 108
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 172,
+ "hold": 146
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 165,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 189,
+ "hold": 44
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 111,
+ "hold": 79
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 141,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 101,
+ "hold": 124
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 134,
+ "hold": 87
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 146,
+ "hold": 144
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 174,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 130,
+ "hold": 79
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 107,
+ "hold": 55
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 200,
+ "hold": 75
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 113,
+ "hold": 110
+ },
+ {
+ "kind": "key",
+ "key": "u",
+ "gap": 130,
+ "hold": 45
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 172,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 156,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 169,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 104,
+ "hold": 98
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 168,
+ "hold": 138
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 205,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 161,
+ "hold": 146
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 206,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": "w",
+ "gap": 188,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 152,
+ "hold": 76
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 177,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 181,
+ "hold": 147
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 157,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 142,
+ "hold": 111
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 179,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 127,
+ "hold": 66
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 91,
+ "hold": 43
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 203,
+ "hold": 132
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 166,
+ "hold": 42
+ },
+ {
+ "kind": "key",
+ "key": "a",
+ "gap": 164,
+ "hold": 105
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 176,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 123,
+ "hold": 148
+ },
+ {
+ "kind": "key",
+ "key": "p",
+ "gap": 168,
+ "hold": 129
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 165,
+ "hold": 131
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 107,
+ "hold": 149
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 151,
+ "hold": 102
+ },
+ {
+ "kind": "key",
+ "key": "i",
+ "gap": 109,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "b",
+ "gap": 115,
+ "hold": 129
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 193,
+ "hold": 121
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 169,
+ "hold": 149
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -6.9,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473759325
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 48.1,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 197.6,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 198.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "wi"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 314.6,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 351,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 352.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "wil"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 436.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 496.5,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 498.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "will"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 562.9,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 652.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 655.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 4,
+ "inputValue": "will ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 772.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 903.1,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 905.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "p"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 959.9,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1000.7,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 0.7
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1084,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1085,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "p"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1180.8,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1198.1,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1198.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "p"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1277.4,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1322.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1323.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "pe"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1421.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1448.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1450.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "peo"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1569.8,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1571.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "peop"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1597.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1683.7,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1685.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": "peopl"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1706.7,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1736.5,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1791.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1793.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 5,
+ "inputValue": "people"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1865.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1947.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1949.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 6,
+ "inputValue": "people ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2000.5,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 0.5
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2073.1,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2182.6,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2185.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "b"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2321.7,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2336.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2337.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "be"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2445.4,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2510.4,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2512,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "bec"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2655.8,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2674.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2677,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "beco"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2815.9,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2865,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2867.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 4,
+ "inputValue": "becom"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2907.8,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2975.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2977,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 5,
+ "inputValue": "become"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3000.3,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 0.3
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3054.8,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3116.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3119.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 6,
+ "inputValue": "become ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3157.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3266.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3267.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3389.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3399.3,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3400.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "op"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3485.3,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3546.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3547.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "ope"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3689.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3719.4,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3720.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "open"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3836.9,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3849,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3850.3,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "open ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3930.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4000.3,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 0.3
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4047.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4050,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4102.6,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4247.9,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4249.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4323.1,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4360.3,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4361.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "s"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4471.4,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4487.4,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4489.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "u",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "su"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4534.9,
+ "data": {
+ "code": "KeyU"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4663,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4665.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "suc"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4804.5,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4817.9,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4820,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "such"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4949.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4985.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4988,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "such ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000.3,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 0.3
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5127.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5240.8,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5242,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 0,
+ "inputValue": "h"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5337.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5409.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5412.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 1,
+ "inputValue": "ho"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5548.1,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5614.4,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5615.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 2,
+ "inputValue": "hol"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5745.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5775.7,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5778.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 3,
+ "inputValue": "hold"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 5922.4,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 5982.3,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 5984.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 5,
+ "charIndex": 4,
+ "inputValue": "hold ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6000.3,
+ "data": {
+ "event": "step",
+ "timer": 6,
+ "drift": 0.3
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6080.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6280,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6282.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "w",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 0,
+ "inputValue": "w"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6319,
+ "data": {
+ "code": "KeyW"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6432.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6435.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 1,
+ "inputValue": "wh"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6507.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6609.5,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6611.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 2,
+ "inputValue": "wha"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6707.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6789.2,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6791.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 3,
+ "inputValue": "what"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 6936.5,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 6944.4,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 6946.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 6,
+ "charIndex": 4,
+ "inputValue": "what ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 7000.4,
+ "data": {
+ "event": "step",
+ "timer": 7,
+ "drift": 0.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7077.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7229.2,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7231.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 0,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7341.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7410,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7412.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7511,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7536.6,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7538.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": false,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputStopped": true,
+ "inputValue": "o"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7603.4,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7627.1,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7629.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 1,
+ "inputValue": "on"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7669.3,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7831.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7833.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 2,
+ "inputValue": "one"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 7962.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 7996.2,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 7997.4,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 7,
+ "charIndex": 3,
+ "inputValue": "one ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 8000.4,
+ "data": {
+ "event": "step",
+ "timer": 8,
+ "drift": 0.4
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8037.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8209.8,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8211.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "a",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 0,
+ "inputValue": "a"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8317.1,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8385.9,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8387.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 1,
+ "inputValue": "at"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8509.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8511,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 8,
+ "charIndex": 2,
+ "inputValue": "at ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8534.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8658,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 8836.2,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 8838.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "p",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 0,
+ "inputValue": "p",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 8963.9,
+ "data": {
+ "code": "KeyP"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9000.5,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9002.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 1,
+ "inputValue": "po",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 9003.6,
+ "data": {
+ "event": "step",
+ "timer": 9,
+ "drift": 3.6
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9105.3,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9105.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 2,
+ "inputValue": "pos",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9130.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9254.5,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9255.4,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9256,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 3,
+ "inputValue": "poss",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9360.4,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9364.6,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9365.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "i",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 4,
+ "inputValue": "possi",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9452.1,
+ "data": {
+ "code": "KeyI"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9482.9,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9484.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "b",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 5,
+ "inputValue": "possib",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9612.4,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9675.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9678.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 6,
+ "inputValue": "possibl",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9797.1,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 9844.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 9846.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 9,
+ "charIndex": 7,
+ "inputValue": "possible",
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 9847.9,
+ "data": {
+ "event": "end",
+ "timer": 9,
+ "date": 1791473769171
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 9923.7,
+ "data": {
+ "code": "KeyE",
+ "estimated": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ "will ",
+ "people ",
+ "become ",
+ "open ",
+ "such ",
+ "hold ",
+ "what ",
+ "one ",
+ "at ",
+ "possible"
+ ],
+ "mode": "words",
+ "mode2": "10",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 65.79,
+ "rawWpm": 65.79,
+ "charStats": [54, 0, 0, 0],
+ "charTotal": 54,
+ "acc": 90,
+ "language": "english",
+ "testDuration": 9.85,
+ "lastKeyToEnd": 3.2,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473769303,
+ "mode": "words",
+ "mode2": "10",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": true,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 79.41,
+ "wpmConsistency": 95.35,
+ "keyConsistency": 71.11,
+ "chartData": {
+ "wpm": [72, 72, 72, 72, 70, 68, 67, 65, 63, 66],
+ "burst": [72, 96, 72, 72, 84, 60, 60, 72, 48, 99],
+ "err": [0, 2, 0, 0, 2, 0, 0, 2, 0, 0]
+ },
+ "keySpacing": [
+ 197.6, 153.4, 145.5, 155.60000000000002, 251, 180.89999999999998,
+ 114.09999999999991, 124.40000000000009, 126.40000000000009,
+ 120.89999999999986, 113.90000000000009, 108.09999999999991,
+ 155.9000000000001, 234.89999999999986, 154.20000000000027,
+ 173.5999999999999, 164.5, 190.0999999999999, 110.19999999999982,
+ 141.60000000000036, 150, 132.5, 147.5999999999999, 172.5,
+ 129.5999999999999, 198.80000000000018, 200.09999999999945,
+ 112.40000000000055, 127.09999999999945, 175.60000000000036,
+ 154.89999999999964, 167.80000000000018, 255.10000000000036,
+ 168.89999999999964, 204.69999999999982, 161.30000000000018,
+ 206.60000000000036, 297.6999999999998, 152.5, 177, 179.69999999999982,
+ 155.19999999999982, 284.8000000000002, 180.80000000000018,
+ 126.60000000000036, 90.5, 204.09999999999945, 165, 213.59999999999945,
+ 176.10000000000036, 123.70000000000073, 326.60000000000036,
+ 164.29999999999927, 104.79999999999927, 150.10000000000036,
+ 109.20000000000073, 118.29999999999927, 192.89999999999964,
+ 168.90000000000146
+ ],
+ "keyDuration": [
+ 55, 117.00000000000003, 85.80000000000001, 66.39999999999998,
+ 120.39999999999998, 56.799999999999955, 96.79999999999995,
+ 79.30000000000018, 99, 148.29999999999995, 136.9000000000001,
+ 52.799999999999955, 73.40000000000009, 125.39999999999986,
+ 139.0999999999999, 108.59999999999991, 145.4000000000001, 141,
+ 42.80000000000018, 79.60000000000036, 40.79999999999973,
+ 122.89999999999964, 86, 143, 117.5, 81.5, 54.80000000000018,
+ 75.20000000000073, 111.09999999999945, 47.5, 141.5, 131.60000000000036,
+ 141.80000000000018, 96.69999999999982, 138.40000000000055,
+ 131.40000000000055, 146.69999999999982, 98.59999999999945, 39, 75,
+ 97.60000000000036, 147.30000000000018, 133.20000000000073,
+ 112.60000000000036, 101, 66.79999999999927, 42.19999999999982,
+ 131.69999999999982, 41.69999999999982, 107.30000000000109,
+ 148.89999999999964, 148.39999999999964, 127.69999999999891,
+ 129.79999999999927, 149.20000000000073, 105, 87.5, 129.5,
+ 121.30000000000109, 79
+ ],
+ "keyOverlap": 100.6,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/keystrokes/zen.json b/packages/typing-fixtures/reference/keystrokes/zen.json
new file mode 100644
index 000000000..a7907feb5
--- /dev/null
+++ b/packages/typing-fixtures/reference/keystrokes/zen.json
@@ -0,0 +1,1106 @@
+{
+ "name": "zen",
+ "description": "zen mode with newlines and a backspace, finished with shift+enter",
+ "recordedWith": "web",
+ "config": {
+ "mode": "zen",
+ "time": 30,
+ "words": 50,
+ "language": "english",
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnError": "off",
+ "confidenceMode": "off",
+ "freedomMode": false,
+ "strictSpace": false,
+ "quickEnd": false,
+ "deleteOnError": "off",
+ "oppositeShiftMode": "off",
+ "britishEnglish": false,
+ "quoteLength": [1]
+ },
+ "keystrokes": [
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 201,
+ "hold": 77
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 117,
+ "hold": 49
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 118,
+ "hold": 96
+ },
+ {
+ "kind": "key",
+ "key": "l",
+ "gap": 99,
+ "hold": 147
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 165,
+ "hold": 116
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 182,
+ "hold": 81
+ },
+ {
+ "kind": "key",
+ "key": "t",
+ "gap": 178,
+ "hold": 136
+ },
+ {
+ "kind": "key",
+ "key": "h",
+ "gap": 153,
+ "hold": 122
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 141,
+ "hold": 94
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 178,
+ "hold": 142
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 208,
+ "hold": 92
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 132,
+ "hold": 127
+ },
+ {
+ "kind": "key",
+ "key": "z",
+ "gap": 122,
+ "hold": 123
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 153,
+ "hold": 78
+ },
+ {
+ "kind": "key",
+ "key": "n",
+ "gap": 145,
+ "hold": 41
+ },
+ {
+ "kind": "key",
+ "key": "Enter",
+ "gap": 148,
+ "hold": 99
+ },
+ {
+ "kind": "key",
+ "key": "m",
+ "gap": 182,
+ "hold": 50
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 187,
+ "hold": 84
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 130,
+ "hold": 100
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 97,
+ "hold": 109
+ },
+ {
+ "kind": "key",
+ "key": "Backspace",
+ "gap": 123,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "Backspace",
+ "gap": 108,
+ "hold": 145
+ },
+ {
+ "kind": "key",
+ "key": "d",
+ "gap": 192,
+ "hold": 141
+ },
+ {
+ "kind": "key",
+ "key": "e",
+ "gap": 132,
+ "hold": 60
+ },
+ {
+ "kind": "key",
+ "key": " ",
+ "gap": 169,
+ "hold": 95
+ },
+ {
+ "kind": "key",
+ "key": "r",
+ "gap": 98,
+ "hold": 124
+ },
+ {
+ "kind": "key",
+ "key": "o",
+ "gap": 205,
+ "hold": 40
+ },
+ {
+ "kind": "key",
+ "key": "c",
+ "gap": 148,
+ "hold": 140
+ },
+ {
+ "kind": "key",
+ "key": "k",
+ "gap": 173,
+ "hold": 101
+ },
+ {
+ "kind": "key",
+ "key": "s",
+ "gap": 208,
+ "hold": 72
+ },
+ {
+ "kind": "key",
+ "key": "Shift+Enter",
+ "gap": 182,
+ "hold": 80
+ }
+ ],
+ "eventLog": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -6.7,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": "h"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 1791473724697
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 67,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 105.7,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 106.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": "he"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 158.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 223.5,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 225.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": "hel"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 322.1,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 324.3,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 326.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "l",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": "hell"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 471.8,
+ "data": {
+ "code": "KeyL"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 489.3,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 491.8,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 4,
+ "inputValue": "hello"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 605.4,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 670.7,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 674.2,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 5,
+ "inputValue": "hello ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 752.8,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 850.7,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 853.2,
+ "data": {
+ "inputType": "insertText",
+ "data": "t",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": "t"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 985.8,
+ "data": {
+ "code": "KeyT"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1000.6,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1002.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "h",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": "th"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1011.6,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 11.6
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1123.5,
+ "data": {
+ "code": "KeyH"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1142.1,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1144.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": "the"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1238,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1320.9,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1323.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": "ther"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1464.2,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1528.9,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1531.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": "there"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1621.5,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1660.5,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1662.7,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 5,
+ "inputValue": "there ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1783.1,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1785.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "z",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": "z"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1787,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 1904.5,
+ "data": {
+ "code": "KeyZ"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 1936.3,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1938.5,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": "ze"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2011.7,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 11.7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2015,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2081.7,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2083.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "n",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 2,
+ "inputValue": "zen"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2121.5,
+ "data": {
+ "code": "KeyN"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2229.8,
+ "data": {
+ "code": "Enter"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2233.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "\n",
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 3,
+ "inputValue": "zen\n",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2330.7,
+ "data": {
+ "code": "Enter"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2412.8,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2415.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "m",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": "m"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2461.1,
+ "data": {
+ "code": "KeyM"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2598.8,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2601.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": "mo"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2683,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2729.2,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2730.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "moe"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 2826.3,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2827.9,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "moed"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2830.4,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 2934.3,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 2948.5,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "moe"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3011.7,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 11.7
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3055.7,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "mo"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3249.4,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3251.7,
+ "data": {
+ "inputType": "insertText",
+ "data": "d",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 2,
+ "inputValue": "mod"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3379.6,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3381.3,
+ "data": {
+ "inputType": "insertText",
+ "data": "e",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 3,
+ "inputValue": "mode"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3387.1,
+ "data": {
+ "code": "KeyD"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3440,
+ "data": {
+ "code": "KeyE"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3548.9,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3551.1,
+ "data": {
+ "inputType": "insertText",
+ "data": " ",
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 4,
+ "inputValue": "mode ",
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3642.6,
+ "data": {
+ "code": "Space"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3643.6,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3644.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "r",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 0,
+ "inputValue": "r"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3770.3,
+ "data": {
+ "code": "KeyR"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3851.7,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 3854.4,
+ "data": {
+ "inputType": "insertText",
+ "data": "o",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 1,
+ "inputValue": "ro"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 3891,
+ "data": {
+ "code": "KeyO"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 3999.1,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4001.1,
+ "data": {
+ "inputType": "insertText",
+ "data": "c",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 2,
+ "inputValue": "roc"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4011.7,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 11.7
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4141.6,
+ "data": {
+ "code": "KeyC"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4173.5,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4176,
+ "data": {
+ "inputType": "insertText",
+ "data": "k",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 3,
+ "inputValue": "rock"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4273.8,
+ "data": {
+ "code": "KeyK"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4379.8,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 4381.6,
+ "data": {
+ "inputType": "insertText",
+ "data": "s",
+ "correct": true,
+ "wordIndex": 4,
+ "charIndex": 4,
+ "inputValue": "rocks"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4452.7,
+ "data": {
+ "code": "KeyS"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 4560.4,
+ "data": {
+ "code": "Enter",
+ "shift": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4561.4,
+ "data": {
+ "event": "end",
+ "timer": 4,
+ "date": 1791473729257
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 4582.8,
+ "data": {
+ "code": "Enter",
+ "shift": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [],
+ "mode": "zen",
+ "mode2": "zen",
+ "koreanStatus": false,
+ "bailedOut": false
+ }
+ },
+ "completedEvent": {
+ "wpm": 68.42,
+ "rawWpm": 68.42,
+ "charStats": [26, 0, 0, 0],
+ "charTotal": 26,
+ "acc": 100,
+ "language": "english",
+ "testDuration": 4.56,
+ "lastKeyToEnd": 0,
+ "startToFirstKey": 0,
+ "afkDuration": 0,
+ "tags": [],
+ "punctuation": false,
+ "numbers": false,
+ "lazyMode": false,
+ "timestamp": 1791473729386,
+ "mode": "zen",
+ "mode2": "zen",
+ "bailedOut": false,
+ "funbox": [],
+ "difficulty": "normal",
+ "blindMode": false,
+ "stopOnLetter": false,
+ "restartCount": 0,
+ "incompleteTests": [],
+ "incompleteTestSeconds": 0,
+ "consistency": 86.36,
+ "wpmConsistency": 90.89,
+ "keyConsistency": 62.49,
+ "chartData": {
+ "wpm": [84, 84, 76, 69, 68],
+ "burst": [84, 84, 72, 60, 64],
+ "err": [0, 0, 0, 0, 0]
+ },
+ "keySpacing": [
+ 105.7, 117.8, 100.80000000000001, 165, 181.40000000000003, 180,
+ 149.89999999999998, 141.4999999999999, 178.80000000000018, 208,
+ 131.5999999999999, 122.59999999999991, 153.20000000000005,
+ 145.39999999999986, 148.10000000000036, 183, 186, 130.39999999999964,
+ 97.10000000000036, 423.0999999999999, 130.19999999999982,
+ 169.30000000000018, 94.69999999999982, 208.0999999999999,
+ 147.4000000000001, 174.4000000000001, 206.30000000000018,
+ 180.59999999999945
+ ],
+ "keyDuration": [
+ 73.7, 53.2, 98.60000000000002, 147.5, 116.09999999999997,
+ 82.09999999999991, 135.0999999999999, 122.89999999999998,
+ 95.90000000000009, 143.29999999999995, 92.59999999999991, 126.5,
+ 121.40000000000009, 78.70000000000005, 39.80000000000018,
+ 100.89999999999964, 48.29999999999973, 84.19999999999982,
+ 101.20000000000027, 108, 137.69999999999982, 60.40000000000009,
+ 93.69999999999982, 126.70000000000027, 39.30000000000018,
+ 142.50000000000045, 100.30000000000018, 72.89999999999964,
+ 22.400000000000546
+ ],
+ "keyOverlap": 15.5,
+ "client": "web"
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-newlines-tabs.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-newlines-tabs.json
new file mode 100644
index 000000000..ff4ceac09
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-newlines-tabs.json
@@ -0,0 +1,10 @@
+{
+ "words": ["line\n", "\tindented ", "word ", "end\n"],
+ "sectionIndexes": [1, 2, 3, 4],
+ "hasTab": true,
+ "hasNewline": true,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-random.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-random.json
new file mode 100644
index 000000000..9c74d0081
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-random.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "delta ",
+ "epsilon ",
+ "beta ",
+ "alpha ",
+ "gamma ",
+ "beta ",
+ "alpha ",
+ "delta ",
+ "beta ",
+ "epsilon ",
+ "gamma ",
+ "zeta ",
+ "epsilon ",
+ "alpha ",
+ "beta ",
+ "delta ",
+ "alpha ",
+ "beta ",
+ "delta ",
+ "alpha ",
+ "zeta ",
+ "gamma ",
+ "epsilon ",
+ "beta ",
+ "gamma ",
+ "zeta ",
+ "beta ",
+ "gamma ",
+ "delta ",
+ "epsilon "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-repeat.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-repeat.json
new file mode 100644
index 000000000..248b1801e
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-repeat.json
@@ -0,0 +1,20 @@
+{
+ "words": [
+ "The ",
+ "quick ",
+ "brown ",
+ "fox ",
+ "jumps ",
+ "over ",
+ "the ",
+ "lazy ",
+ "dog "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-repeated.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-repeated.json
new file mode 100644
index 000000000..6a8954501
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-repeated.json
@@ -0,0 +1,23 @@
+{
+ "words": [
+ "cyan ",
+ "green ",
+ "red ",
+ "cyan ",
+ "green ",
+ "blue ",
+ "cyan ",
+ "green ",
+ "blue ",
+ "red ",
+ "green ",
+ "cyan "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-sections.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-sections.json
new file mode 100644
index 000000000..a36cefd4c
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-sections.json
@@ -0,0 +1,25 @@
+{
+ "words": [
+ "a ",
+ "dog ",
+ "ran ",
+ "on ",
+ "the ",
+ "mat ",
+ "far ",
+ "away ",
+ "the ",
+ "cat ",
+ "sat ",
+ "on ",
+ "the ",
+ "mat "
+ ],
+ "sectionIndexes": [1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 5, 5, 5],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-shuffle.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-shuffle.json
new file mode 100644
index 000000000..f1e84d494
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-shuffle.json
@@ -0,0 +1,21 @@
+{
+ "words": [
+ "four ",
+ "three ",
+ "one ",
+ "two ",
+ "five ",
+ "two ",
+ "one ",
+ "three ",
+ "five ",
+ "four "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/custom-time.json b/packages/typing-fixtures/reference/words/__snapshots__/custom-time.json
new file mode 100644
index 000000000..543a5b3ae
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/custom-time.json
@@ -0,0 +1,144 @@
+{
+ "words": [
+ "tick ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tock ",
+ "tock ",
+ "tick ",
+ "tock ",
+ "clock ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "clock ",
+ "clock ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "clock ",
+ "tick ",
+ "clock ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "clock ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "clock ",
+ "clock ",
+ "tick ",
+ "tock ",
+ "clock ",
+ "tock ",
+ "tock ",
+ "clock ",
+ "tick ",
+ "clock ",
+ "tick ",
+ "clock ",
+ "tock ",
+ "tock ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "tock ",
+ "tick ",
+ "tick ",
+ "tick ",
+ "clock ",
+ "clock ",
+ "tick ",
+ "clock ",
+ "clock ",
+ "tick ",
+ "tock ",
+ "tock ",
+ "tick ",
+ "tock ",
+ "clock ",
+ "clock ",
+ "tick ",
+ "tock ",
+ "clock ",
+ "tock ",
+ "tick ",
+ "clock ",
+ "tock ",
+ "tock ",
+ "clock ",
+ "clock ",
+ "tock ",
+ "tock ",
+ "tock ",
+ "clock ",
+ "tock ",
+ "clock ",
+ "clock ",
+ "clock "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+ 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
+ 98, 99, 100
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [
+ {
+ "word": "tock ",
+ "wordRaw": "tock",
+ "sectionIndex": 101
+ },
+ {
+ "word": "tock ",
+ "wordRaw": "tock",
+ "sectionIndex": 102
+ },
+ {
+ "word": "tock ",
+ "wordRaw": "tock",
+ "sectionIndex": 103
+ },
+ {
+ "word": "tock ",
+ "wordRaw": "tock",
+ "sectionIndex": 104
+ },
+ {
+ "word": "tick ",
+ "wordRaw": "tick",
+ "sectionIndex": 105
+ }
+ ],
+ "allWordsGenerated": false,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-58008.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-58008.json
new file mode 100644
index 000000000..bd63eb722
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-58008.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "36 ",
+ "89.706 ",
+ "646 ",
+ "8536.4 ",
+ "86 ",
+ "36/1144 ",
+ "172 ",
+ "199 ",
+ "23.1*7 ",
+ "1.32 ",
+ "8 ",
+ "558*21 ",
+ "17.5 ",
+ "75*9 ",
+ "6.11039 ",
+ "7408 ",
+ "1 ",
+ "5 ",
+ "960027 ",
+ "8972.42 "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-ALL_CAPS.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-ALL_CAPS.json
new file mode 100644
index 000000000..a75c65044
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-ALL_CAPS.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "WHILE ",
+ "THAN ",
+ "NATION ",
+ "LIFE ",
+ "THING ",
+ "GIVE ",
+ "NO ",
+ "WORK ",
+ "FROM ",
+ "LEAD ",
+ "GOOD ",
+ "PRESENT ",
+ "SEEM ",
+ "MUCH ",
+ "WITH ",
+ "ASK ",
+ "BEGIN ",
+ "OLD ",
+ "BACK ",
+ "GREAT "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv4.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv4.json
new file mode 100644
index 000000000..052bc0b79
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv4.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "227.248.0.0/13 ",
+ "243.79.31.133 ",
+ "154.96.0.0/12 ",
+ "117.73.216.147 ",
+ "232.133.195.121 ",
+ "134.179.192.0/18 ",
+ "107.81.191.198 ",
+ "18.242.174.219 ",
+ "9.163.90.84 ",
+ "188.65.0.0/17 ",
+ "96.56.0.0/14 ",
+ "24.146.209.77 ",
+ "23.41.122.235 ",
+ "66.2.209.109 ",
+ "164.147.203.63 ",
+ "164.196.251.103 ",
+ "86.119.99.196 ",
+ "145.178.56.56 ",
+ "132.246.71.250 ",
+ "81.30.67.0/24 "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv6.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv6.json
new file mode 100644
index 000000000..e85ab324b
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-IPv6.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "b8d3:fecc:805f:9d52:539d:e0e3:128b:ce68 ",
+ "dcdb:6c49:3ee5:6ee5:1200:43d7:e9c0:a19d ",
+ "2960:4dfd:17d4:56d5:5200:9dcf:7770:6dbc ",
+ "f1dd:dd38:b030:9186:e26c:20a:2299:10cb ",
+ "4ee4:3855:10e4:d0a9:8286:13af:80ca:44c2 ",
+ "c5f1:9340:6cc0:225:2033:92fe:66c1:54ce ",
+ "4870:f047:b1e:5bed:b761:d5a0:7bc8:be36 ",
+ "eab9:7101:74cd:e02b:ec6c:a355:b6ff:4aef ",
+ "bacd:1ad4:4d89:6e2c:4730:f016:1f8d:a1dd ",
+ "c8d1:2d25:47d:287a:fc73:e713:1047:f6ae ",
+ "2926:827c:aaec:e639:817b:6de2:9a1c:f0b2 ",
+ "757c:4ec6:f09e:dd8:476c:8a7f:6c8b:c25e ",
+ "f246:82b1:50fa:f339:f400::/72 ",
+ "7b0e:c8da:2789:295a:bca:2ea0:46e8:ea3a ",
+ "f576:b0ee:9717:b2af:5f56:b540:fbfe:402 ",
+ "c489:cd5f:798d:ab55:b05a:98e:373a:78cd ",
+ "4502:9749:4642:cbf2:1a5b:eb3e:99a3:7d55 ",
+ "b1b0:5cb5:d1d6:6b88:a21b:9955:c33b:401f ",
+ "8324:f005:fed9:3cd4:6e3e:bd7b:35ff:ecd5 ",
+ "ffdb:656d:d33a:86c8:c9da:3799:5a6:6663 "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-arrows.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-arrows.json
new file mode 100644
index 000000000..43b4998b4
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-arrows.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "→←→↑",
+ "↓↑→←",
+ "↑←→↓",
+ "↑←↓↑",
+ "→←→↓",
+ "↑↓→←",
+ "↑←→↑",
+ "→←↓↑",
+ "→←→↓",
+ "↑↓→←",
+ "→↑↓←",
+ "→←↑↓",
+ "→↑↓←",
+ "↓↑→↓",
+ "↑←→↓",
+ "→←↓↑",
+ "↓←→↓",
+ "↑↓→←",
+ "↓←→↑",
+ "↓←→↓"
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-ascii.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-ascii.json
new file mode 100644
index 000000000..544e1dec0
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-ascii.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "6 ",
+ "r%!T:! ",
+ "YBq}D]w1/d ",
+ "2% ",
+ "JY2 ",
+ "+=W ",
+ "}h.VeU@ ",
+ "MD88Dq&E4 ",
+ "oGEq[AILv/ ",
+ "9%# ",
+ "7 ",
+ "I ",
+ "vWV&Gp#T: ",
+ "v% ",
+ "AM! ",
+ ".; ",
+ ". ",
+ ">@~%+ ",
+ "?$\"./_ ",
+ "|#: ",
+ "#$`}\\? ",
+ ".. ",
+ "[@=&\"`< ",
+ "`_{)(~` ",
+ "?|#-? ",
+ ";%.#}< ",
+ "$ ",
+ "|!\\$ ",
+ ")={$ ",
+ "%\\# "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-underscore_spaces.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-underscore_spaces.json
new file mode 100644
index 000000000..80d328ae9
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-underscore_spaces.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "off_",
+ "follow_",
+ "feel_",
+ "mean_",
+ "because_",
+ "such_",
+ "eye_",
+ "most_",
+ "first_",
+ "under_",
+ "before_",
+ "plan_",
+ "how_",
+ "home_",
+ "if_",
+ "develop_",
+ "over_",
+ "present_",
+ "by_",
+ "all"
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-weakspot.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-weakspot.json
new file mode 100644
index 000000000..ff98f4faf
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-weakspot.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "all ",
+ "write ",
+ "give ",
+ "first ",
+ "early ",
+ "plan ",
+ "thing ",
+ "from ",
+ "old ",
+ "school ",
+ "stand ",
+ "will ",
+ "move ",
+ "back ",
+ "just ",
+ "move ",
+ "then ",
+ "few ",
+ "as ",
+ "own "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/funbox-zipf.json b/packages/typing-fixtures/reference/words/__snapshots__/funbox-zipf.json
new file mode 100644
index 000000000..bd4e070e1
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/funbox-zipf.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "in ",
+ "the ",
+ "there ",
+ "it ",
+ "the ",
+ "now ",
+ "because ",
+ "only ",
+ "the ",
+ "be ",
+ "life ",
+ "not ",
+ "it ",
+ "all ",
+ "in ",
+ "the ",
+ "to ",
+ "that ",
+ "possible ",
+ "the "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/quote-all.json b/packages/typing-fixtures/reference/words/__snapshots__/quote-all.json
new file mode 100644
index 000000000..db18157bf
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/quote-all.json
@@ -0,0 +1,28 @@
+{
+ "words": [
+ "And ",
+ "every ",
+ "time ",
+ "I've ",
+ "held ",
+ "a ",
+ "rose, ",
+ "it ",
+ "seems ",
+ "I ",
+ "only ",
+ "felt ",
+ "the ",
+ "thorns. "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": {
+ "id": 5649,
+ "group": 0
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/quote-british-english.json b/packages/typing-fixtures/reference/words/__snapshots__/quote-british-english.json
new file mode 100644
index 000000000..96756f1a0
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/quote-british-english.json
@@ -0,0 +1,26 @@
+{
+ "words": [
+ "If ",
+ "there ",
+ "is ",
+ "magic ",
+ "on ",
+ "this ",
+ "planet, ",
+ "it ",
+ "is ",
+ "contained ",
+ "in ",
+ "water. "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": {
+ "id": 7027,
+ "group": 0
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/quote-by-id.json b/packages/typing-fixtures/reference/words/__snapshots__/quote-by-id.json
new file mode 100644
index 000000000..be07042dc
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/quote-by-id.json
@@ -0,0 +1,29 @@
+{
+ "words": [
+ "The ",
+ "person ",
+ "that ",
+ "has ",
+ "the ",
+ "most ",
+ "to ",
+ "do ",
+ "with ",
+ "what ",
+ "happens ",
+ "to ",
+ "you ",
+ "is ",
+ "you! "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": {
+ "id": 13,
+ "group": 0
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/quote-repeated.json b/packages/typing-fixtures/reference/words/__snapshots__/quote-repeated.json
new file mode 100644
index 000000000..c500783e1
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/quote-repeated.json
@@ -0,0 +1,30 @@
+{
+ "words": [
+ "I'm ",
+ "having ",
+ "a ",
+ "birthday ",
+ "party, ",
+ "but ",
+ "you're ",
+ "not ",
+ "invited, ",
+ "but ",
+ "you ",
+ "can ",
+ "come ",
+ "if ",
+ "you ",
+ "want. "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": {
+ "id": 3780,
+ "group": 0
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/quote-short.json b/packages/typing-fixtures/reference/words/__snapshots__/quote-short.json
new file mode 100644
index 000000000..b9daea05d
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/quote-short.json
@@ -0,0 +1,31 @@
+{
+ "words": [
+ "She ",
+ "had ",
+ "waited ",
+ "all ",
+ "her ",
+ "life ",
+ "for ",
+ "something ",
+ "and ",
+ "it ",
+ "had ",
+ "killed ",
+ "her ",
+ "when ",
+ "it ",
+ "found ",
+ "her. "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": {
+ "id": 59,
+ "group": 0
+ }
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/time-15-english-1k.json b/packages/typing-fixtures/reference/words/__snapshots__/time-15-english-1k.json
new file mode 100644
index 000000000..9b81e9327
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/time-15-english-1k.json
@@ -0,0 +1,118 @@
+{
+ "words": [
+ "also ",
+ "century ",
+ "plain ",
+ "smile ",
+ "bright ",
+ "inch ",
+ "dollar ",
+ "left ",
+ "present ",
+ "them ",
+ "material ",
+ "has ",
+ "winter ",
+ "keep ",
+ "put ",
+ "substance ",
+ "twenty ",
+ "than ",
+ "through ",
+ "all ",
+ "summer ",
+ "clear ",
+ "practice ",
+ "expect ",
+ "all ",
+ "stay ",
+ "step ",
+ "ball ",
+ "measure ",
+ "speak ",
+ "under ",
+ "opposite ",
+ "product ",
+ "lost ",
+ "sugar ",
+ "arm ",
+ "poor ",
+ "difficult ",
+ "ear ",
+ "red ",
+ "book ",
+ "take ",
+ "feel ",
+ "universe ",
+ "joy ",
+ "certain ",
+ "an ",
+ "mind ",
+ "capital ",
+ "program ",
+ "open ",
+ "opposite ",
+ "has ",
+ "ride ",
+ "length ",
+ "ask ",
+ "fall ",
+ "man ",
+ "made ",
+ "teeth ",
+ "allow ",
+ "again ",
+ "one ",
+ "spend ",
+ "solve ",
+ "thick ",
+ "power ",
+ "tail ",
+ "word ",
+ "field ",
+ "plain ",
+ "receive ",
+ "of ",
+ "practice ",
+ "ten ",
+ "got ",
+ "support ",
+ "gray ",
+ "spot ",
+ "month ",
+ "method ",
+ "substance ",
+ "feel ",
+ "may ",
+ "fun ",
+ "cold ",
+ "order ",
+ "salt ",
+ "product ",
+ "believe ",
+ "idea ",
+ "blow ",
+ "reason ",
+ "certain ",
+ "choose ",
+ "speak ",
+ "join ",
+ "hurry ",
+ "interest ",
+ "mark "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+ 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
+ 98, 99, 100
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": false,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/time-30.json b/packages/typing-fixtures/reference/words/__snapshots__/time-30.json
new file mode 100644
index 000000000..a60980901
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/time-30.json
@@ -0,0 +1,169 @@
+{
+ "words": [
+ "turn ",
+ "he ",
+ "long ",
+ "with ",
+ "child ",
+ "people ",
+ "down ",
+ "will ",
+ "into ",
+ "little ",
+ "can ",
+ "not ",
+ "number ",
+ "well ",
+ "late ",
+ "head ",
+ "use ",
+ "same ",
+ "year ",
+ "thing ",
+ "seem ",
+ "follow ",
+ "high ",
+ "of ",
+ "head ",
+ "know ",
+ "run ",
+ "few ",
+ "many ",
+ "number ",
+ "would ",
+ "such ",
+ "turn ",
+ "the ",
+ "be ",
+ "well ",
+ "govern ",
+ "become ",
+ "school ",
+ "ask ",
+ "old ",
+ "with ",
+ "give ",
+ "how ",
+ "during ",
+ "after ",
+ "where ",
+ "just ",
+ "nation ",
+ "since ",
+ "hold ",
+ "right ",
+ "give ",
+ "never ",
+ "another ",
+ "much ",
+ "early ",
+ "consider ",
+ "number ",
+ "general ",
+ "fact ",
+ "few ",
+ "for ",
+ "high ",
+ "around ",
+ "first ",
+ "too ",
+ "year ",
+ "where ",
+ "little ",
+ "hand ",
+ "even ",
+ "off ",
+ "say ",
+ "fact ",
+ "call ",
+ "form ",
+ "few ",
+ "they ",
+ "after ",
+ "would ",
+ "same ",
+ "small ",
+ "both ",
+ "change ",
+ "help ",
+ "course ",
+ "old ",
+ "word ",
+ "fact ",
+ "person ",
+ "see ",
+ "between ",
+ "keep ",
+ "even ",
+ "write ",
+ "which ",
+ "need ",
+ "place ",
+ "than "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+ 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
+ 98, 99, 100
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [
+ {
+ "word": "consider ",
+ "wordRaw": "consider",
+ "sectionIndex": 101
+ },
+ {
+ "word": "it ",
+ "wordRaw": "it",
+ "sectionIndex": 102
+ },
+ {
+ "word": "without ",
+ "wordRaw": "without",
+ "sectionIndex": 103
+ },
+ {
+ "word": "on ",
+ "wordRaw": "on",
+ "sectionIndex": 104
+ },
+ {
+ "word": "many ",
+ "wordRaw": "many",
+ "sectionIndex": 105
+ },
+ {
+ "word": "word ",
+ "wordRaw": "word",
+ "sectionIndex": 106
+ },
+ {
+ "word": "to ",
+ "wordRaw": "to",
+ "sectionIndex": 107
+ },
+ {
+ "word": "thing ",
+ "wordRaw": "thing",
+ "sectionIndex": 108
+ },
+ {
+ "word": "by ",
+ "wordRaw": "by",
+ "sectionIndex": 109
+ },
+ {
+ "word": "year ",
+ "wordRaw": "year",
+ "sectionIndex": 110
+ }
+ ],
+ "allWordsGenerated": false,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-0-infinite.json b/packages/typing-fixtures/reference/words/__snapshots__/words-0-infinite.json
new file mode 100644
index 000000000..8da5ccf0d
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-0-infinite.json
@@ -0,0 +1,144 @@
+{
+ "words": [
+ "leave ",
+ "use ",
+ "year ",
+ "high ",
+ "head ",
+ "need ",
+ "just ",
+ "now ",
+ "against ",
+ "we ",
+ "ask ",
+ "if ",
+ "many ",
+ "follow ",
+ "old ",
+ "which ",
+ "thing ",
+ "such ",
+ "increase ",
+ "he ",
+ "set ",
+ "they ",
+ "but ",
+ "can ",
+ "be ",
+ "keep ",
+ "through ",
+ "need ",
+ "change ",
+ "what ",
+ "much ",
+ "any ",
+ "that ",
+ "feel ",
+ "at ",
+ "for ",
+ "play ",
+ "large ",
+ "the ",
+ "course ",
+ "with ",
+ "increase ",
+ "seem ",
+ "can ",
+ "general ",
+ "that ",
+ "since ",
+ "system ",
+ "public ",
+ "back ",
+ "face ",
+ "with ",
+ "number ",
+ "course ",
+ "during ",
+ "call ",
+ "feel ",
+ "in ",
+ "without ",
+ "during ",
+ "place ",
+ "know ",
+ "however ",
+ "eye ",
+ "back ",
+ "any ",
+ "and ",
+ "fact ",
+ "know ",
+ "before ",
+ "home ",
+ "around ",
+ "set ",
+ "look ",
+ "no ",
+ "develop ",
+ "both ",
+ "number ",
+ "run ",
+ "no ",
+ "only ",
+ "any ",
+ "high ",
+ "to ",
+ "year ",
+ "follow ",
+ "there ",
+ "since ",
+ "even ",
+ "way ",
+ "find ",
+ "present ",
+ "well ",
+ "change ",
+ "than ",
+ "on ",
+ "while ",
+ "a ",
+ "leave ",
+ "against "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+ 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
+ 98, 99, 100
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [
+ {
+ "word": "develop ",
+ "wordRaw": "develop",
+ "sectionIndex": 101
+ },
+ {
+ "word": "year ",
+ "wordRaw": "year",
+ "sectionIndex": 102
+ },
+ {
+ "word": "get ",
+ "wordRaw": "get",
+ "sectionIndex": 103
+ },
+ {
+ "word": "under ",
+ "wordRaw": "under",
+ "sectionIndex": 104
+ },
+ {
+ "word": "this ",
+ "wordRaw": "this",
+ "sectionIndex": 105
+ }
+ ],
+ "allWordsGenerated": false,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-10.json b/packages/typing-fixtures/reference/words/__snapshots__/words-10.json
new file mode 100644
index 000000000..a5702f9d3
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-10.json
@@ -0,0 +1,21 @@
+{
+ "words": [
+ "between ",
+ "the ",
+ "seem ",
+ "course ",
+ "stand ",
+ "new ",
+ "show ",
+ "turn ",
+ "must ",
+ "help "
+ ],
+ "sectionIndexes": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-100-show-all-lines.json b/packages/typing-fixtures/reference/words/__snapshots__/words-100-show-all-lines.json
new file mode 100644
index 000000000..eea8df2ae
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-100-show-all-lines.json
@@ -0,0 +1,171 @@
+{
+ "words": [
+ "of ",
+ "they ",
+ "early ",
+ "another ",
+ "feel ",
+ "also ",
+ "much ",
+ "time ",
+ "little ",
+ "real ",
+ "than ",
+ "all ",
+ "child ",
+ "those ",
+ "more ",
+ "such ",
+ "some ",
+ "how ",
+ "take ",
+ "early ",
+ "go ",
+ "head ",
+ "can ",
+ "which ",
+ "would ",
+ "some ",
+ "place ",
+ "child ",
+ "with ",
+ "system ",
+ "little ",
+ "both ",
+ "school ",
+ "stand ",
+ "both ",
+ "order ",
+ "public ",
+ "he ",
+ "say ",
+ "now ",
+ "must ",
+ "a ",
+ "on ",
+ "few ",
+ "such ",
+ "must ",
+ "much ",
+ "face ",
+ "begin ",
+ "never ",
+ "like ",
+ "now ",
+ "or ",
+ "from ",
+ "system ",
+ "use ",
+ "can ",
+ "need ",
+ "then ",
+ "have ",
+ "school ",
+ "order ",
+ "place ",
+ "public ",
+ "last ",
+ "set ",
+ "you ",
+ "seem ",
+ "develop ",
+ "program ",
+ "play ",
+ "during ",
+ "tell ",
+ "play ",
+ "stand ",
+ "place ",
+ "end ",
+ "be ",
+ "those ",
+ "get ",
+ "turn ",
+ "place ",
+ "off ",
+ "early ",
+ "world ",
+ "open ",
+ "must ",
+ "call ",
+ "might ",
+ "little ",
+ "know ",
+ "time ",
+ "be ",
+ "a ",
+ "keep ",
+ "follow ",
+ "need ",
+ "of ",
+ "house ",
+ "tell ",
+ "part ",
+ "help ",
+ "only ",
+ "change ",
+ "develop ",
+ "interest ",
+ "both ",
+ "follow ",
+ "or ",
+ "help ",
+ "good ",
+ "still ",
+ "will ",
+ "find ",
+ "man ",
+ "person ",
+ "govern ",
+ "might ",
+ "last ",
+ "help ",
+ "here ",
+ "might ",
+ "group ",
+ "down ",
+ "so ",
+ "old ",
+ "home ",
+ "again ",
+ "when ",
+ "little ",
+ "well ",
+ "seem ",
+ "make ",
+ "about ",
+ "before ",
+ "well ",
+ "say ",
+ "the ",
+ "a ",
+ "this ",
+ "head ",
+ "course ",
+ "they ",
+ "say ",
+ "also ",
+ "just ",
+ "leave ",
+ "such ",
+ "may ",
+ "late "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
+ 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
+ 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+ 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,
+ 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142,
+ 143, 144, 145, 146, 147, 148, 149, 150
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-20-repeated.json b/packages/typing-fixtures/reference/words/__snapshots__/words-20-repeated.json
new file mode 100644
index 000000000..942c5bc7a
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-20-repeated.json
@@ -0,0 +1,33 @@
+{
+ "words": [
+ "back ",
+ "must ",
+ "each ",
+ "work ",
+ "can ",
+ "must ",
+ "thing ",
+ "of ",
+ "run ",
+ "head ",
+ "may ",
+ "they ",
+ "off ",
+ "of ",
+ "will ",
+ "then ",
+ "must ",
+ "plan ",
+ "call ",
+ "than "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-25-numbers.json b/packages/typing-fixtures/reference/words/__snapshots__/words-25-numbers.json
new file mode 100644
index 000000000..c27da7ecb
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-25-numbers.json
@@ -0,0 +1,39 @@
+{
+ "words": [
+ "never ",
+ "out ",
+ "on ",
+ "turn ",
+ "lead ",
+ "since ",
+ "38 ",
+ "year ",
+ "turn ",
+ "and ",
+ "no ",
+ "do ",
+ "help ",
+ "not ",
+ "see ",
+ "change ",
+ "most ",
+ "around ",
+ "head ",
+ "large ",
+ "or ",
+ "hand ",
+ "since ",
+ "be ",
+ "first "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-25-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-25-punctuation.json
new file mode 100644
index 000000000..33a6f07a1
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-25-punctuation.json
@@ -0,0 +1,39 @@
+{
+ "words": [
+ "Order ",
+ "like ",
+ "leave ",
+ "- ",
+ "down, ",
+ "both ",
+ "'I' ",
+ "people ",
+ "time ",
+ "few ",
+ "not. ",
+ "Move ",
+ "you ",
+ "here ",
+ "line ",
+ "show ",
+ "with ",
+ "however ",
+ "or ",
+ "should, ",
+ "will ",
+ "run ",
+ "might ",
+ "part ",
+ "face. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-arabic-numbers.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-arabic-numbers.json
new file mode 100644
index 000000000..c9f5a2150
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-arabic-numbers.json
@@ -0,0 +1,45 @@
+{
+ "words": [
+ "فَضَلَكَ ",
+ "4 ",
+ "الْفَضَاءُ ",
+ "93 ",
+ "مُدْهِشٌ، ",
+ "بِالطَّبْعِ، ",
+ "هَادِئٌ ",
+ "الْوَقْتُ ",
+ "الْمَنْزِلُ، ",
+ "بِالطَّبْعِ، ",
+ "الذَّهَابُ ",
+ "الثَّالِثَةَ ",
+ "أَلِأرْضٍ ",
+ "مُمَارَسَةٌ. ",
+ "مُضَاعَفَةٌ ",
+ "عَظِيمٌ ",
+ "دَرَجَةٌ ",
+ "بِالطَّبْعِ ",
+ "مُضَاعَفَةٌ ",
+ "عَظِيمٌ ",
+ "125 ",
+ "خِلَالَ ",
+ "وَظِيفَةٌ ",
+ "الْمُفَضَّلُ ",
+ "الذَّهَابُ ",
+ "حَجْمً ",
+ "دُكْتورٌ ",
+ "مَاذَا، ",
+ "'مَسَاءَ' ",
+ "صَدِيقِي. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allRightToLeft": true,
+ "allJoiningScript": true,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-code-javascript-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-code-javascript-punctuation.json
new file mode 100644
index 000000000..d15bd34da
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-code-javascript-punctuation.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "Symbol(). ",
+ "createUploadthing ",
+ "onmouseup ",
+ "[].map() ",
+ "Math.trunc(), ",
+ "i++ ",
+ "includes(). ",
+ "this ",
+ "onmouseup, ",
+ "onNext() ",
+ ".input ",
+ "Math.pow(), ",
+ "return ",
+ "in. ",
+ "do ",
+ "{ ",
+ "isNaN(), ",
+ "|| ",
+ "nodeType ",
+ "- ",
+ "` ",
+ "[ ",
+ "% ",
+ "trim(), ",
+ "await ",
+ "attributes ",
+ "object ",
+ "switch ",
+ "console ",
+ "await. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-french-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-french-punctuation.json
new file mode 100644
index 000000000..839b1c64b
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-french-punctuation.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "Les ",
+ "trois ",
+ "forme ",
+ "juste, ",
+ "côté ",
+ "trois, ",
+ "port ",
+ "après ",
+ "(essayer) ",
+ "côté ",
+ "penser ",
+ "temps ",
+ "est ",
+ "venu ",
+ "air ",
+ "de ",
+ "fabriqué ",
+ "est, ",
+ "année ",
+ "utiliser, ",
+ "père ",
+ "eau ",
+ "première ",
+ "où ",
+ "les ",
+ "trois ",
+ "appel ",
+ "pourrait ",
+ "plus ",
+ "image. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-german.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-german.json
new file mode 100644
index 000000000..6a74dea3c
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-german.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "drei ",
+ "neuen ",
+ "ihren ",
+ "nun ",
+ "wo ",
+ "bereits ",
+ "nicht ",
+ "damit ",
+ "ihren ",
+ "was ",
+ "muss ",
+ "gut ",
+ "Uhr ",
+ "Zeit ",
+ "ins ",
+ "einer ",
+ "sind ",
+ "sein ",
+ "diese ",
+ "eines ",
+ "selbst ",
+ "er ",
+ "etwa ",
+ "sondern ",
+ "Uhr ",
+ "gut ",
+ "denn ",
+ "Unternehmen ",
+ "können ",
+ "seien "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-french.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-french.json
new file mode 100644
index 000000000..e9d3fd432
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-french.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "notre ",
+ "seulement ",
+ "air ",
+ "penser ",
+ "etre ",
+ "pourrait ",
+ "notre ",
+ "chaque ",
+ "avec ",
+ "signifier ",
+ "autre ",
+ "faut ",
+ "de ",
+ "comme ",
+ "changement ",
+ "signifier ",
+ "haut ",
+ "eu ",
+ "vivre ",
+ "petit ",
+ "meme ",
+ "penser ",
+ "notre ",
+ "je ",
+ "montrer ",
+ "ces ",
+ "penser ",
+ "animal ",
+ "trois ",
+ "juste "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-polish-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-polish-punctuation.json
new file mode 100644
index 000000000..59f8fd36f
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-lazy-polish-punctuation.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "Brzmiec ",
+ "pozniej ",
+ "dlaczego ",
+ "potrzeba ",
+ "pisac ",
+ "(kiedy) ",
+ "wziac ",
+ "mnie ",
+ "odnalezc ",
+ "dzien, ",
+ "wielki ",
+ "zrobic ",
+ "czas. ",
+ "Miejsce ",
+ "dzien ",
+ "dwa ",
+ "puszka ",
+ "pomoc. ",
+ "Formularz ",
+ "gdzie, ",
+ "przed ",
+ "maly ",
+ "jest ",
+ "kto ",
+ "przyczyna ",
+ "mnie, ",
+ "odnalezc ",
+ "szukac ",
+ "zespol ",
+ "niz. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-spanish-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-spanish-punctuation.json
new file mode 100644
index 000000000..04ceed0d8
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-spanish-punctuation.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "¡Dicho ",
+ "de ",
+ "gente ",
+ "camino ",
+ "este, ",
+ "años ",
+ "causa, ",
+ "hasta ",
+ "muchos ",
+ "dar ",
+ "- ",
+ "tomar! ",
+ "Si ",
+ "eran ",
+ "segundo ",
+ "- ",
+ "cuerpo ",
+ "animal ",
+ "si, ",
+ "tenido ",
+ "poder ",
+ "algunos, ",
+ "todo ",
+ "sistema ",
+ "otros ",
+ "con ",
+ "hizo ",
+ "para ",
+ "cambio ",
+ "poco "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-swiss-german.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-swiss-german.json
new file mode 100644
index 000000000..9176b2f95
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-swiss-german.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "sind ",
+ "Zvieri ",
+ "aus ",
+ "Teil ",
+ "unsere ",
+ "denken ",
+ "für ",
+ "unten ",
+ "oben ",
+ "Kopf ",
+ "könnte ",
+ "Trottinett ",
+ "Welt ",
+ "hatte ",
+ "alt ",
+ "Trinken ",
+ "zwei ",
+ "jeder ",
+ "warum ",
+ "Znüni ",
+ "leben ",
+ "wenn ",
+ "Satz ",
+ "zu ",
+ "selbst ",
+ "müssen ",
+ "parkieren ",
+ "sein ",
+ "niedrig ",
+ "wollen "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-30-turkish-punctuation.json b/packages/typing-fixtures/reference/words/__snapshots__/words-30-turkish-punctuation.json
new file mode 100644
index 000000000..950bad8af
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-30-turkish-punctuation.json
@@ -0,0 +1,44 @@
+{
+ "words": [
+ "İle ",
+ "- ",
+ "anlam ",
+ "nasıl, ",
+ "bazı ",
+ "çıkarmak ",
+ "anlatmak ",
+ "dönmek ",
+ "grup ",
+ "yol, ",
+ "zor ",
+ "etki ",
+ "gelmek ",
+ "an ",
+ "kitap ",
+ "böyle ",
+ "o ",
+ "'tek' ",
+ "sorun ",
+ "(ülke) ",
+ "gelen ",
+ "kendi, ",
+ "arkadaş ",
+ "diye ",
+ "geri ",
+ "yapmak ",
+ "özellik ",
+ "bunu ",
+ "açmak, ",
+ "insan. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-50-british-english.json b/packages/typing-fixtures/reference/words/__snapshots__/words-50-british-english.json
new file mode 100644
index 000000000..7c070ef92
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-50-british-english.json
@@ -0,0 +1,65 @@
+{
+ "words": [
+ "Universe ",
+ "book, ",
+ "ease ",
+ "about ",
+ "farm? ",
+ "Success ",
+ "wide ",
+ "door ",
+ "happen ",
+ "agree ",
+ "fun. ",
+ "Sister ",
+ "multiply, ",
+ "red ",
+ "white ",
+ "only ",
+ "dear ",
+ "open ",
+ "drop ",
+ "ball ",
+ "fell, ",
+ "receive ",
+ "condition ",
+ "love ",
+ "wall ",
+ "father ",
+ "send ",
+ "rest ",
+ "tie ",
+ "'leave' ",
+ "train ",
+ "soil. ",
+ "Period ",
+ "neck, ",
+ "modern ",
+ "shape, ",
+ "atom ",
+ "pretty! ",
+ "Single ",
+ "wall. ",
+ "Connect ",
+ "should, ",
+ "fell ",
+ "nation ",
+ "possible, ",
+ "join ",
+ "problem, ",
+ "add ",
+ "gentle ",
+ "protect. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-50-english-punctuation-contractions.json b/packages/typing-fixtures/reference/words/__snapshots__/words-50-english-punctuation-contractions.json
new file mode 100644
index 000000000..6532ce725
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-50-english-punctuation-contractions.json
@@ -0,0 +1,65 @@
+{
+ "words": [
+ "Become ",
+ "back ",
+ "- ",
+ "shouldn't ",
+ "present ",
+ "no, ",
+ "last ",
+ "may ",
+ "open ",
+ "take ",
+ "know, ",
+ "while ",
+ "late: ",
+ "might, ",
+ "while ",
+ "say ",
+ "just ",
+ "same ",
+ "person, ",
+ "house ",
+ "I ",
+ "work ",
+ "and. ",
+ "Real ",
+ "group ",
+ "part ",
+ "these, ",
+ "the ",
+ "so ",
+ "who. ",
+ "For ",
+ "into ",
+ "if ",
+ "own ",
+ "move ",
+ "around ",
+ "when ",
+ "house ",
+ "not. ",
+ "Begin ",
+ "couldn't ",
+ "turn, ",
+ "for ",
+ "leave ",
+ "late ",
+ "last ",
+ "it'll ",
+ "on ",
+ "public ",
+ "may. "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/words-50-punctuation-numbers.json b/packages/typing-fixtures/reference/words/__snapshots__/words-50-punctuation-numbers.json
new file mode 100644
index 000000000..7ac4a1cae
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/words-50-punctuation-numbers.json
@@ -0,0 +1,65 @@
+{
+ "words": [
+ "250 ",
+ "the ",
+ "also ",
+ "out ",
+ "number ",
+ "than ",
+ "for ",
+ "all, ",
+ "take ",
+ "can: ",
+ "that, ",
+ "program ",
+ "plan ",
+ "around. ",
+ "Seem ",
+ "not ",
+ "eye ",
+ "to. ",
+ "Who ",
+ "early ",
+ "of ",
+ "course, ",
+ "head ",
+ "when ",
+ "some, ",
+ "into ",
+ "make ",
+ "by ",
+ "school, ",
+ "change ",
+ "the ",
+ "96 ",
+ "people ",
+ "another, ",
+ "open ",
+ "who ",
+ "out ",
+ "'I' ",
+ "98 ",
+ "number ",
+ "a ",
+ "3015 ",
+ "7814 ",
+ "some ",
+ "long, ",
+ "public ",
+ "when, ",
+ "could ",
+ "mean ",
+ "no! "
+ ],
+ "sectionIndexes": [
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
+ 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50
+ ],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": true,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/__snapshots__/zen.json b/packages/typing-fixtures/reference/words/__snapshots__/zen.json
new file mode 100644
index 000000000..9d04943d5
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/__snapshots__/zen.json
@@ -0,0 +1,10 @@
+{
+ "words": [],
+ "sectionIndexes": [],
+ "hasTab": false,
+ "hasNewline": false,
+ "allJoiningScript": false,
+ "extra": [],
+ "allWordsGenerated": false,
+ "currentQuote": null
+}
diff --git a/packages/typing-fixtures/reference/words/cases.json b/packages/typing-fixtures/reference/words/cases.json
new file mode 100644
index 000000000..eae7165ef
--- /dev/null
+++ b/packages/typing-fixtures/reference/words/cases.json
@@ -0,0 +1,687 @@
+[
+ {
+ "name": "words-10",
+ "phase": "base",
+ "seed": 1,
+ "config": {
+ "mode": "words",
+ "words": 10
+ }
+ },
+ {
+ "name": "words-0-infinite",
+ "phase": "base",
+ "seed": 2,
+ "config": {
+ "mode": "words",
+ "words": 0
+ },
+ "extraWords": 5
+ },
+ {
+ "name": "time-30",
+ "phase": "base",
+ "seed": 3,
+ "config": {
+ "mode": "time",
+ "time": 30
+ },
+ "extraWords": 10
+ },
+ {
+ "name": "words-25-punctuation",
+ "phase": "base",
+ "seed": 4,
+ "config": {
+ "mode": "words",
+ "words": 25,
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-25-numbers",
+ "phase": "base",
+ "seed": 5,
+ "config": {
+ "mode": "words",
+ "words": 25,
+ "numbers": true
+ }
+ },
+ {
+ "name": "words-50-punctuation-numbers",
+ "phase": "base",
+ "seed": 6,
+ "config": {
+ "mode": "words",
+ "words": 50,
+ "punctuation": true,
+ "numbers": true
+ }
+ },
+ {
+ "name": "words-100-show-all-lines",
+ "phase": "base",
+ "seed": 7,
+ "config": {
+ "mode": "words",
+ "words": 150,
+ "showAllLines": true
+ }
+ },
+ {
+ "name": "time-15-english-1k",
+ "phase": "base",
+ "seed": 8,
+ "config": {
+ "mode": "time",
+ "time": 15,
+ "language": "english_1k"
+ }
+ },
+ {
+ "name": "words-30-spanish-punctuation",
+ "phase": "base",
+ "seed": 9,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "spanish",
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-30-french-punctuation",
+ "phase": "base",
+ "seed": 10,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "french",
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-30-german",
+ "phase": "base",
+ "seed": 11,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "german"
+ }
+ },
+ {
+ "name": "words-30-code-javascript-punctuation",
+ "phase": "base",
+ "seed": 12,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "code_javascript",
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-30-arabic-numbers",
+ "phase": "base",
+ "seed": 13,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "arabic",
+ "numbers": true,
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-20-repeated",
+ "phase": "base",
+ "seed": 14,
+ "config": {
+ "mode": "words",
+ "words": 20
+ },
+ "repeat": true
+ },
+ {
+ "name": "quote-short",
+ "phase": "quote-custom-zen",
+ "seed": 20,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [0]
+ }
+ },
+ {
+ "name": "quote-all",
+ "phase": "quote-custom-zen",
+ "seed": 21,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [0, 1, 2, 3]
+ }
+ },
+ {
+ "name": "quote-repeated",
+ "phase": "quote-custom-zen",
+ "seed": 22,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [1]
+ },
+ "repeat": true
+ },
+ {
+ "name": "quote-by-id",
+ "phase": "quote-custom-zen",
+ "seed": 23,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [-2]
+ },
+ "selectedQuoteId": 13
+ },
+ {
+ "name": "custom-repeat",
+ "phase": "quote-custom-zen",
+ "seed": 24,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": [
+ "The",
+ "quick",
+ "brown",
+ "fox",
+ "jumps",
+ "over",
+ "the",
+ "lazy",
+ "dog"
+ ],
+ "mode": "repeat",
+ "limit": {
+ "value": 9,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ }
+ },
+ {
+ "name": "custom-random",
+ "phase": "quote-custom-zen",
+ "seed": 25,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"],
+ "mode": "random",
+ "limit": {
+ "value": 30,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ }
+ },
+ {
+ "name": "custom-shuffle",
+ "phase": "quote-custom-zen",
+ "seed": 26,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["one", "two", "three", "four", "five"],
+ "mode": "shuffle",
+ "limit": {
+ "value": 10,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ }
+ },
+ {
+ "name": "custom-time",
+ "phase": "quote-custom-zen",
+ "seed": 27,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["tick", "tock", "clock"],
+ "mode": "random",
+ "limit": {
+ "value": 30,
+ "mode": "time"
+ },
+ "pipeDelimiter": false
+ },
+ "extraWords": 5
+ },
+ {
+ "name": "custom-sections",
+ "phase": "quote-custom-zen",
+ "seed": 28,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["the cat sat", "on the mat", "a dog ran", "far away"],
+ "mode": "random",
+ "limit": {
+ "value": 5,
+ "mode": "section"
+ },
+ "pipeDelimiter": true
+ }
+ },
+ {
+ "name": "custom-newlines-tabs",
+ "phase": "quote-custom-zen",
+ "seed": 29,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["line\n", "\tindented", "word", "end\n"],
+ "mode": "repeat",
+ "limit": {
+ "value": 4,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ }
+ },
+ {
+ "name": "custom-repeated",
+ "phase": "quote-custom-zen",
+ "seed": 30,
+ "config": {
+ "mode": "custom"
+ },
+ "customText": {
+ "text": ["red", "green", "blue", "cyan"],
+ "mode": "random",
+ "limit": {
+ "value": 12,
+ "mode": "word"
+ },
+ "pipeDelimiter": false
+ },
+ "repeat": true
+ },
+ {
+ "name": "zen",
+ "phase": "quote-custom-zen",
+ "seed": 31,
+ "config": {
+ "mode": "zen"
+ }
+ },
+ {
+ "name": "words-30-lazy-french",
+ "phase": "language-helpers",
+ "seed": 40,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "french",
+ "lazyMode": true
+ }
+ },
+ {
+ "name": "words-30-lazy-polish-punctuation",
+ "phase": "language-helpers",
+ "seed": 41,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "polish",
+ "lazyMode": true,
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-50-english-punctuation-contractions",
+ "phase": "language-helpers",
+ "seed": 42,
+ "config": {
+ "mode": "words",
+ "words": 50,
+ "punctuation": true
+ }
+ },
+ {
+ "name": "words-50-british-english",
+ "phase": "language-helpers",
+ "seed": 43,
+ "config": {
+ "mode": "words",
+ "words": 50,
+ "language": "english_1k",
+ "britishEnglish": true,
+ "punctuation": true
+ }
+ },
+ {
+ "name": "quote-british-english",
+ "phase": "language-helpers",
+ "seed": 44,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [2],
+ "britishEnglish": true
+ }
+ },
+ {
+ "name": "words-30-swiss-german",
+ "phase": "language-helpers",
+ "seed": 45,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "swiss_german"
+ }
+ },
+ {
+ "name": "words-30-turkish-punctuation",
+ "phase": "language-helpers",
+ "seed": 46,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "language": "turkish",
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-58008",
+ "phase": "funbox",
+ "seed": 60,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["58008"],
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-arrows",
+ "phase": "funbox",
+ "seed": 61,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["arrows"]
+ }
+ },
+ {
+ "name": "funbox-rAnDoMcAsE",
+ "phase": "funbox",
+ "seed": 62,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["rAnDoMcAsE"]
+ }
+ },
+ {
+ "name": "funbox-sPoNgEcAsE",
+ "phase": "funbox",
+ "seed": 63,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["sPoNgEcAsE"]
+ }
+ },
+ {
+ "name": "funbox-capitals",
+ "phase": "funbox",
+ "seed": 64,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["capitals"]
+ }
+ },
+ {
+ "name": "funbox-gibberish",
+ "phase": "funbox",
+ "seed": 65,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["gibberish"]
+ }
+ },
+ {
+ "name": "funbox-ascii",
+ "phase": "funbox",
+ "seed": 66,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["ascii"]
+ }
+ },
+ {
+ "name": "funbox-specials",
+ "phase": "funbox",
+ "seed": 67,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["specials"]
+ }
+ },
+ {
+ "name": "funbox-plus_one",
+ "phase": "funbox",
+ "seed": 68,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["plus_one"]
+ }
+ },
+ {
+ "name": "funbox-plus_three",
+ "phase": "funbox",
+ "seed": 69,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["plus_three"]
+ }
+ },
+ {
+ "name": "funbox-nospace",
+ "phase": "funbox",
+ "seed": 70,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["nospace"]
+ }
+ },
+ {
+ "name": "funbox-weakspot",
+ "phase": "funbox",
+ "seed": 71,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["weakspot"]
+ }
+ },
+ {
+ "name": "funbox-pseudolang",
+ "phase": "funbox",
+ "seed": 72,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["pseudolang"]
+ }
+ },
+ {
+ "name": "funbox-IPv4",
+ "phase": "funbox",
+ "seed": 73,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["IPv4"],
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-IPv6",
+ "phase": "funbox",
+ "seed": 74,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["IPv6"],
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-binary",
+ "phase": "funbox",
+ "seed": 75,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["binary"]
+ }
+ },
+ {
+ "name": "funbox-hexadecimal",
+ "phase": "funbox",
+ "seed": 76,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["hexadecimal"],
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-zipf",
+ "phase": "funbox",
+ "seed": 77,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["zipf"]
+ }
+ },
+ {
+ "name": "funbox-morse",
+ "phase": "funbox",
+ "seed": 78,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["morse"]
+ }
+ },
+ {
+ "name": "funbox-backwards",
+ "phase": "funbox",
+ "seed": 79,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["backwards"]
+ }
+ },
+ {
+ "name": "funbox-ddoouubblleedd",
+ "phase": "funbox",
+ "seed": 80,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["ddoouubblleedd"]
+ }
+ },
+ {
+ "name": "funbox-instant_messaging",
+ "phase": "funbox",
+ "seed": 81,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["instant_messaging"],
+ "punctuation": true
+ }
+ },
+ {
+ "name": "funbox-underscore_spaces",
+ "phase": "funbox",
+ "seed": 82,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["underscore_spaces"]
+ }
+ },
+ {
+ "name": "funbox-ALL_CAPS",
+ "phase": "funbox",
+ "seed": 83,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["ALL_CAPS"]
+ }
+ },
+ {
+ "name": "funbox-rot13",
+ "phase": "funbox",
+ "seed": 84,
+ "config": {
+ "mode": "words",
+ "words": 20,
+ "funbox": ["rot13"]
+ }
+ },
+ {
+ "name": "funbox-polyglot",
+ "phase": "funbox",
+ "seed": 85,
+ "config": {
+ "mode": "words",
+ "words": 30,
+ "funbox": ["polyglot"],
+ "customPolyglot": ["english", "spanish", "french"]
+ }
+ },
+ {
+ "name": "funbox-rot13-capitals-time",
+ "phase": "funbox",
+ "seed": 86,
+ "config": {
+ "mode": "time",
+ "time": 30,
+ "funbox": ["rot13", "capitals"]
+ },
+ "extraWords": 5
+ },
+ {
+ "name": "funbox-backwards-quote",
+ "phase": "funbox",
+ "seed": 87,
+ "config": {
+ "mode": "quote",
+ "quoteLength": [0],
+ "funbox": ["backwards"]
+ }
+ }
+]
From 0890a7f2ddc0d71f3c1c52f792b3cfe47f940f69 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Thu, 22 Oct 2026 23:51:28 +0530
Subject: [PATCH 04/93] test(engine): freeze UTF-16 commands and intermediate
TypeScript oracle
---
docs/rust-engine/compatibility.md | 26 +
docs/rust-engine/contract.md | 122 +++
docs/rust-engine/decisions.md | 13 +
docs/rust-engine/progress.md | 12 +-
.../typing-core/__test__/rust-oracle.spec.ts | 54 ++
.../scripts/capture-rust-oracle.ts | 17 +
packages/typing-core/scripts/rust-oracle.ts | 148 +++
packages/typing-fixtures/commands.schema.json | 65 ++
.../typing-fixtures/commands/code-indent.json | 30 +
.../commands/delete-error-letter.json | 34 +
.../commands/delete-error-letter_hard.json | 34 +
.../commands/delete-error-word.json | 34 +
.../commands/delete-error-word_hard.json | 34 +
.../typing-fixtures/commands/finite-edit.json | 51 ++
.../commands/freedom-regression.json | 34 +
.../commands/ime-typography.json | 25 +
.../commands/infinite-words.json | 26 +
.../commands/master-finish-once.json | 37 +
.../typing-fixtures/commands/stop-letter.json | 24 +
.../typing-fixtures/commands/timer-stall.json | 38 +
.../commands/unicode-halves.json | 33 +
.../typing-fixtures/oracle/code-indent.json | 478 ++++++++++
.../oracle/delete-error-letter.json | 564 ++++++++++++
.../oracle/delete-error-letter_hard.json | 640 +++++++++++++
.../oracle/delete-error-word.json | 520 +++++++++++
.../oracle/delete-error-word_hard.json | 552 ++++++++++++
.../typing-fixtures/oracle/finite-edit.json | 842 ++++++++++++++++++
.../oracle/freedom-regression.json | 437 +++++++++
.../oracle/ime-typography.json | 190 ++++
.../oracle/infinite-words.json | 509 +++++++++++
.../oracle/master-finish-once.json | 323 +++++++
.../typing-fixtures/oracle/stop-letter.json | 206 +++++
.../typing-fixtures/oracle/timer-stall.json | 339 +++++++
.../oracle/unicode-halves.json | 282 ++++++
34 files changed, 6769 insertions(+), 4 deletions(-)
create mode 100644 docs/rust-engine/compatibility.md
create mode 100644 docs/rust-engine/contract.md
create mode 100644 docs/rust-engine/decisions.md
create mode 100644 packages/typing-core/__test__/rust-oracle.spec.ts
create mode 100644 packages/typing-core/scripts/capture-rust-oracle.ts
create mode 100644 packages/typing-core/scripts/rust-oracle.ts
create mode 100644 packages/typing-fixtures/commands.schema.json
create mode 100644 packages/typing-fixtures/commands/code-indent.json
create mode 100644 packages/typing-fixtures/commands/delete-error-letter.json
create mode 100644 packages/typing-fixtures/commands/delete-error-letter_hard.json
create mode 100644 packages/typing-fixtures/commands/delete-error-word.json
create mode 100644 packages/typing-fixtures/commands/delete-error-word_hard.json
create mode 100644 packages/typing-fixtures/commands/finite-edit.json
create mode 100644 packages/typing-fixtures/commands/freedom-regression.json
create mode 100644 packages/typing-fixtures/commands/ime-typography.json
create mode 100644 packages/typing-fixtures/commands/infinite-words.json
create mode 100644 packages/typing-fixtures/commands/master-finish-once.json
create mode 100644 packages/typing-fixtures/commands/stop-letter.json
create mode 100644 packages/typing-fixtures/commands/timer-stall.json
create mode 100644 packages/typing-fixtures/commands/unicode-halves.json
create mode 100644 packages/typing-fixtures/oracle/code-indent.json
create mode 100644 packages/typing-fixtures/oracle/delete-error-letter.json
create mode 100644 packages/typing-fixtures/oracle/delete-error-letter_hard.json
create mode 100644 packages/typing-fixtures/oracle/delete-error-word.json
create mode 100644 packages/typing-fixtures/oracle/delete-error-word_hard.json
create mode 100644 packages/typing-fixtures/oracle/finite-edit.json
create mode 100644 packages/typing-fixtures/oracle/freedom-regression.json
create mode 100644 packages/typing-fixtures/oracle/ime-typography.json
create mode 100644 packages/typing-fixtures/oracle/infinite-words.json
create mode 100644 packages/typing-fixtures/oracle/master-finish-once.json
create mode 100644 packages/typing-fixtures/oracle/stop-letter.json
create mode 100644 packages/typing-fixtures/oracle/timer-stall.json
create mode 100644 packages/typing-fixtures/oracle/unicode-halves.json
diff --git a/docs/rust-engine/compatibility.md b/docs/rust-engine/compatibility.md
new file mode 100644
index 000000000..46f0d4fee
--- /dev/null
+++ b/docs/rust-engine/compatibility.md
@@ -0,0 +1,26 @@
+# Compatibility coverage map
+
+Never regenerate existing reference snapshots. New oracle traces were captured
+once from the unchanged TypeScript session; capture refuses existing output.
+
+| Contract | Reference / additional required verification |
+| --- | --- |
+| Per-unit commits, edits, finish/reset | commands/finite-edit; existing session tests |
+| Isolated UTF-16 halves, combining marks | commands/unicode-halves |
+| IME metadata, typography/space normalization | commands/ime-typography; browser IME characterization required 1.4 |
+| Backtracking | commands/freedom-regression; Firefox clearedNextWord browser case required 1.4 |
+| Automatic code tabs/unindent | commands/code-indent; deferred browser task ordering required 1.4 |
+| Timer stalls and finish/reset races | commands/timer-stall; existing timer specs |
+| Infinite limits | commands/infinite-words; timed/zen zero-limit cases required 1.4 |
+| Error policies | commands/delete-error-*; stop-letter; master-finish-once |
+| Generation / random consumption | reference/generator + 61 existing snapshots; connect native/Wasm at 1.5 |
+| Scores, hashes, history, replay | reference/keystrokes + snapshots; original frontend characterization at 1.6 |
+| Backend rejection behavior | reference/anticheat; backend/__tests__/anticheat |
+| Browser-only DOM/composition behavior | frontend/__tests__/input; e2e/migration-characterization; expand for recorded differences in contract |
+| Transport schema | packages/schemas/src/results.ts; mechanical binding/CompletedEvent check at 1.6–1.7 |
+| SSR and immutable asset pairing | Start delivery/update suites at 1.3 and 2.5 |
+
+These are classified gaps, not coverage claims. Phase 1.2 freezes the comparison
+mechanism; later phases must fill their respective gaps before their gates pass.
+The deliberate-mismatch test changes the first word index and requires comparison
+to fail; fixture-integrity test checks every original and copied file hash.
diff --git a/docs/rust-engine/contract.md b/docs/rust-engine/contract.md
new file mode 100644
index 000000000..556c999af
--- /dev/null
+++ b/docs/rust-engine/contract.md
@@ -0,0 +1,122 @@
+# Engine contract v1
+
+Binding version 1; behavior version 1; stored web event-log version 1. These are
+independent. Reject a binding version mismatch before creating a session. Existing
+logs keep version 1 and their interpretation. This migration changes no score rule.
+
+## Units and representation
+
+`Text = u16[]`: JavaScript UTF-16 code units, including isolated surrogates. All
+target words, committed text, input snapshots and composition text cross the
+neutral boundary as Text. Rust stores these losslessly; never use `chars().count()`
+for `charIndex`, input length, slicing, or scoring. Browser glue converts Text with
+bounded `String.fromCharCode` chunks and `charCodeAt`. UTF-8 conversion is allowed
+only for host display, with explicit replacement of isolated surrogates. Terminal
+graphemes/cells are display units; retain UTF-16 offsets in results.
+
+`now`: finite monotonic milliseconds, host-captured. Recorder rounds to two decimal
+places using existing JS roundTo2 semantics. Wall-clock `date` is separate explicit
+Unix milliseconds, supplied by host, never used for input intervals. Native host
+uses Instant for now. Equal timestamps preserve capture order internally; stored
+logs preserve the legacy stable tie ordering: keyup, keydown, input, composition,
+timer (input before composition when tied, matching recorder group merge).
+
+Optional properties are omitted, never replaced by false/null. Legacy JSON converts
+nonfinite derived numbers to null. Published rounded scores require exact equality;
+unrounded float fields compare exact IEEE-754 values unless a documented field
+tolerance is established by cross-platform evidence. No blanket epsilon.
+
+## Ownership and commands
+
+One independent session owns word index, input history, rule config, recorder,
+timer, generator, pace and completion. No global state, implicit clock, I/O, DOM,
+callback that overrides progression, or platform RNG. Hosts own scheduling,
+observations, assets, rendering, storage, transport and account fields.
+
+Commands are synchronous after module/content preparation. Initial proof commands:
+
+- `insert {data: Text, now, date, correctShiftUsed?, automatic?, isCompositionEnding?}`
+- `delete {kind: deleteContentBackward|deleteWordBackward, now, date}`
+- `start`, `advance`, `finish {reason?}`, `end`, `abandon` with now/date
+- `observe {type: keydown|keyup|composition|timer, data, now}`
+- `reset`: retain prepared content, clear recorder/timer/input/finish state
+
+Full port adds config projection/update, browser observations, content requests,
+generation/repeat, pace/weak-spot operations, result derivation and historical replay.
+Typed Rust declarations must define these before use; proof coverage does not imply
+the full surface is ready. Full snapshots/event logs are explicit reads; hot-path
+updates contain ordered new events, changed words, cursor/current input, status,
+tick/finish notifications. No growing full log copied per key.
+
+## Lifecycle and failures
+
+Created → ready → active → ended; reset returns ready. Finish emits once. End and
+abandon stop silently; abandon measures incomplete engaged time. After ended,
+typing/ticks cannot restart; release observations can finalize held keys until
+disposal. Disposal releases instance allocation; repeated disposal is harmless;
+all other stale-handle commands fail structurally. A reset increments a generation
+token. Async assets and queued automatic indentation carry that token; late replies
+cannot affect another generation. Initialization failure leaves input disabled and
+allows retry with a new instance. Commands validate before mutation.
+
+Content loading is host-owned. Rust requests a typed token; host supplies validated
+content with the matching token/generation. While awaiting content, block new typing
+as the current web does; do not queue unbounded text. Reset cancels pending requests.
+Prepare content ahead where possible. Infinite generation stays bounded per request.
+
+## Browser observations that affect existing behavior
+
+Beforeinput observes actual textarea/sentinel deletion range, word visibility,
+overflow measurements, modifiers/Shift correctness, composition state and captured
+time. Rust decides acceptance/progression. DOM absence must not silently become
+freedom to backtrack beyond the old visible window. Firefox can delete sentinel
+plus residue; preserve clearedNextWord. Host callbacks do not supply word index.
+
+Known headless/web differences requiring recorded web cases before input cutover:
+
+- Web checks failure/completion after the last multi-input unit and outside active
+ composition; headless session checks every unit.
+- Web schedules automatic tabs on a later task; headless session inserts recursively.
+- Web records DOM-based limits/visible words; headless session has no observations.
+- Web normalizes prior to ellipsis expansion; headless path checks blocking first.
+
+Do not assume passing TestSession fixtures proves these paths. Existing frontend
+handler tests/browser characterization remain authoritative; add cases before 1.4.
+
+## Configuration and random source
+
+Legacy settings migration, labels, themes, display preferences remain TS and SSR
+safe. Engine config is a normalized projection, never the entire web preferences
+object. Preserve current effective live rule reads; capture config at each command.
+Generation reads config at its existing generation boundaries. Do not freeze a
+currently live setting silently. Rust defaults/capabilities generate checked host
+metadata; transport Zod schemas remain authoritative and mechanically checked.
+
+Fixtures supply the exact floating-point random stream in [0,1); exhaustion is an
+error, not reseeding. Production uses a specified seeded algorithm, host-provided
+seed, stable draw order; seed algorithm/behavior version accompanies native history.
+Legacy generator snapshots require their existing seedrandom stream, not matching
+only distributions. Host asset loaders supply English punctuation/replacement data;
+Rust decides transform order and random consumption.
+
+## Results, replay and host transport
+
+Rust derives shared scores/histories/eligibility and replay from versioned logs.
+Physical observations are optional capabilities. No keyup capability means durations
+unavailable, never invented zeros/releases. Browser synthetic releases retain the
+existing estimated marker. Native summaries distinguish unavailable from measured
+zero. Terminal stores no account credentials and submits no web results.
+
+Web host adds uid/tags/challenge/network metadata and runs the existing object-hash
+helper after those fields. Preserve schemas, omitted fields and backend validation.
+Hashing is transport, not a Rust JSON hash. Full historical fixtures and anticheat
+rejection tests must pass before removing TS result/replay implementations.
+
+## Differential evidence
+
+Immutable original fixtures: packages/typing-fixtures/reference, copied byte-for-byte
+from the starting commit; original hashes in reference-manifest.json. New command
+fixtures: commands/*.json, with frozen TypeScript frames in oracle/*.json.
+Compare every command's state and events, then final derived values. A harness
+self-test must reject a deliberately altered word index. New captures refuse to
+overwrite files. Existing snapshots are never updated by the harness.
diff --git a/docs/rust-engine/decisions.md b/docs/rust-engine/decisions.md
new file mode 100644
index 000000000..d20322e9e
--- /dev/null
+++ b/docs/rust-engine/decisions.md
@@ -0,0 +1,13 @@
+# Migration decisions
+
+- Sole implementation owner; three stacked stage PRs, as requested.
+- Text boundary uses UTF-16 unit arrays. Rust String cannot preserve an isolated
+ surrogate; unit arrays avoid a lossy JSON string roundtrip and work natively.
+- Keep original fixtures during Stage 1; neutral reference package holds exact
+ copies, checked by SHA-256. Removal of old package waits for Stage 2 gate.
+- Record web/headless differences in contract. Headless session is an oracle for
+ matching cases, not permission to change browser behavior.
+- Use installed Rust 1.99.1 as candidate pin. Confirm target/tool compatibility
+ before finalizing Phase 1.3; no lower MSRV support claimed without a build.
+- Offline terminal initial scope excludes accounts/sync. Publication follows
+ repository release authorization; no production deployment implied by this task.
diff --git a/docs/rust-engine/progress.md b/docs/rust-engine/progress.md
index 6cb95b86d..058151e82 100644
--- a/docs/rust-engine/progress.md
+++ b/docs/rust-engine/progress.md
@@ -12,15 +12,15 @@ empty commits to inflate count. Do not publish production or releases implicitly
## Current position
-Stage 1, Phase 1.1 complete; Phase 1.2 in progress. Read root AGENTS.md and the complete migration
+Stage 1, Phases 1.1–1.2 complete; Phase 1.3 in progress. Read root AGENTS.md and the complete migration
plan before edits. Installed workspace dependencies with frozen lockfile.
-Phase 1.1 gate passed: inventory, feature map, reproducible browser/performance baseline. Existing website engine remains authoritative.
+Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen oracle and mismatch detection. Existing website engine remains authoritative.
| Phase | Status | Evidence / remaining gate |
| --- | --- | --- |
| 1.1 Inventory / baselines | Complete | See inventory.md, features.md, baseline.md and raw measurements. |
-| 1.2 Contracts / fixtures | Active | Freeze neutral command contract and differential harness. |
-| 1.3 Native / browser proof | Pending | Requires frozen contracts. |
+| 1.2 Contracts / fixtures | Complete | 13 command traces, 15 oracle/integrity tests; contract and classified gaps. |
+| 1.3 Native / browser proof | Active | Implement finite slice; native/browser/Cloudflare parity and measured budgets pending. |
| 1.4 Input / lifecycle / timing | Pending | Requires production browser proof. |
| 1.5 Generation / configuration | Pending | Requires contract and proof. |
| 1.6 Events / results / replay | Pending | Requires input and generation. |
@@ -35,6 +35,10 @@ Phase 1.1 gate passed: inventory, feature map, reproducible browser/performance
- Environment: Linux, Node 24.21.0; installed Rust 1.99.1; GitHub CLI authenticated.
Toolchain/binding pins not yet selected. Browser/OS support unverified.
+- Phase 1.2: `vp test run packages/typing-core/__test__/rust-oracle.spec.ts`: 15 pass.
+- Phase 1.2 type-aware lint: pass. Capture created new traces once, refuses overwrite.
+- `vp exec rustup target add wasm32-unknown-unknown`: pass.
+
## Decisions
- User's three-stage PR structure overrides the plan's suggested per-phase PRs.
diff --git a/packages/typing-core/__test__/rust-oracle.spec.ts b/packages/typing-core/__test__/rust-oracle.spec.ts
new file mode 100644
index 000000000..fb62126bf
--- /dev/null
+++ b/packages/typing-core/__test__/rust-oracle.spec.ts
@@ -0,0 +1,54 @@
+import { createHash } from "node:crypto";
+import { readdirSync, readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { expect, it } from "vite-plus/test";
+import { readCommandFixture, runOracle } from "../scripts/rust-oracle";
+
+const root = resolve(import.meta.dirname, "../../typing-fixtures");
+const cases = readdirSync(resolve(root, "commands")).filter((name) =>
+ name.endsWith(".json"),
+);
+for (const name of cases) {
+ it(`frozen intermediate command oracle: ${name}`, async () => {
+ const fixture = readCommandFixture(resolve(root, "commands", name));
+ const expected: unknown = JSON.parse(
+ readFileSync(resolve(root, "oracle", name), "utf8"),
+ );
+ expect(await runOracle(fixture)).toEqual(expected);
+ });
+}
+
+it("the harness rejects an introduced progression mismatch", async () => {
+ const fixture = readCommandFixture(
+ resolve(root, "commands/finite-edit.json"),
+ );
+ const actual = await runOracle(fixture);
+ const wrong = structuredClone(actual) as {
+ frames: { state: { wordIndex: number } }[];
+ };
+ const first = wrong.frames[0];
+ if (first === undefined) throw new Error("Missing frame");
+ first.state.wordIndex++;
+ expect(() => expect(actual).toEqual(wrong)).toThrow();
+});
+
+it("original and neutral reference fixtures retain the frozen bytes", () => {
+ const manifest = JSON.parse(
+ readFileSync(
+ resolve(root, "../../docs/rust-engine/reference-manifest.json"),
+ "utf8",
+ ),
+ ) as { sha256: Record };
+ for (const [path, sha256] of Object.entries(manifest.sha256)) {
+ const relative = path.replace("packages/typing-core/__fixtures__/", "");
+ for (const source of [
+ resolve(root, "reference", relative),
+ resolve(root, "../../", path),
+ ]) {
+ expect(
+ createHash("sha256").update(readFileSync(source)).digest("hex"),
+ source,
+ ).toBe(sha256);
+ }
+ }
+});
diff --git a/packages/typing-core/scripts/capture-rust-oracle.ts b/packages/typing-core/scripts/capture-rust-oracle.ts
new file mode 100644
index 000000000..67a62d533
--- /dev/null
+++ b/packages/typing-core/scripts/capture-rust-oracle.ts
@@ -0,0 +1,17 @@
+import { readdirSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { readCommandFixture, runOracle } from "./rust-oracle";
+
+const root = resolve(import.meta.dirname, "../../typing-fixtures");
+// Capture only NEW traces. Deliberately no overwrite/update flag.
+for (const name of readdirSync(resolve(root, "commands")).filter((entry) =>
+ entry.endsWith(".json"),
+)) {
+ const fixture = readCommandFixture(resolve(root, "commands", name));
+ const output = await runOracle(fixture);
+ writeFileSync(
+ resolve(root, "oracle", name),
+ `${JSON.stringify(output, null, 2)}\n`,
+ { flag: "wx" },
+ );
+}
diff --git a/packages/typing-core/scripts/rust-oracle.ts b/packages/typing-core/scripts/rust-oracle.ts
new file mode 100644
index 000000000..1d3b330f4
--- /dev/null
+++ b/packages/typing-core/scripts/rust-oracle.ts
@@ -0,0 +1,148 @@
+import { readFileSync } from "node:fs";
+import { getDefaultConfig } from "../src/config/default-config";
+import { createTestSession, type SessionConfig } from "../src/session";
+import type {
+ EventLog,
+ TestEventData,
+ TestEventType,
+} from "../src/events/types";
+import type { DeleteInputType } from "../src/input/input-type";
+
+export type Text = number[];
+export type OracleCommand = {
+ op:
+ | "insert"
+ | "delete"
+ | "start"
+ | "advance"
+ | "finish"
+ | "end"
+ | "abandon"
+ | "reset"
+ | "observe";
+ now: number;
+ date?: number;
+ data?: Text;
+ kind?: DeleteInputType;
+ reason?: string;
+ correctShiftUsed?: boolean | null;
+ automatic?: true;
+ isCompositionEnding?: true;
+ eventType?: TestEventType;
+ eventData?: TestEventData;
+};
+export type CommandFixture = {
+ name: string;
+ config: Partial;
+ words: Text[];
+ commands: OracleCommand[];
+};
+
+export function units(value: string): Text {
+ return Array.from({ length: value.length }, (_, index) =>
+ value.charCodeAt(index),
+ );
+}
+export function fromUnits(value: Text): string {
+ // Fixtures are bounded; production conversion must use bounded chunks too.
+ let result = "";
+ for (let i = 0; i < value.length; i += 4096) {
+ result += String.fromCharCode(...value.slice(i, i + 4096));
+ }
+ return result;
+}
+export function neutralLog(log: EventLog): unknown {
+ return {
+ ...log,
+ context: {
+ ...log.context,
+ targetWords: log.context.targetWords.map(units),
+ },
+ events: log.events.map((event) => ({
+ ...event,
+ data: {
+ ...event.data,
+ ...("inputValue" in event.data
+ ? { inputValue: units(event.data.inputValue) }
+ : {}),
+ ...("data" in event.data ? { data: units(event.data.data) } : {}),
+ },
+ })),
+ };
+}
+
+export async function runOracle(fixture: CommandFixture): Promise {
+ let date = 0;
+ const session = createTestSession(
+ { ...getDefaultConfig(), ...fixture.config },
+ {
+ words: fixture.words.map(fromUnits),
+ dateNow: () => date,
+ },
+ );
+ const frames: unknown[] = [];
+ for (const command of fixture.commands) {
+ date = command.date ?? 0;
+ switch (command.op) {
+ case "insert":
+ await session.insert(fromUnits(command.data ?? []), command.now, {
+ correctShiftUsed: command.correctShiftUsed,
+ automatic: command.automatic,
+ isCompositionEnding: command.isCompositionEnding,
+ });
+ break;
+ case "delete":
+ session.delete(command.kind ?? "deleteContentBackward", command.now);
+ break;
+ case "start":
+ session.start(command.now);
+ break;
+ case "advance":
+ session.advance(command.now);
+ break;
+ case "finish":
+ session.finish(command.now, command.reason);
+ break;
+ case "end":
+ session.end(command.now);
+ break;
+ case "abandon":
+ session.abandon(command.now);
+ break;
+ case "reset":
+ session.reset();
+ break;
+ case "observe":
+ if (
+ command.eventType === undefined ||
+ command.eventData === undefined
+ ) {
+ throw new Error("Missing observation");
+ }
+ session.record(command.eventType, command.now, command.eventData);
+ break;
+ }
+ const log = session.buildEventLog();
+ frames.push({
+ state: {
+ wordIndex: session.getActiveWordIndex(),
+ inputValue: units(session.recorder.getCurrentInput()),
+ active: session.isActive(),
+ ended: log.events.some(
+ (event) => event.type === "timer" && event.data.event === "end",
+ ),
+ ...(session.getFinishReason() === undefined
+ ? {}
+ : { reason: session.getFinishReason() }),
+ },
+ log: neutralLog(log),
+ });
+ }
+ // JSON is the legacy transport: omit undefined and map nonfinite values to null.
+ // oxlint-disable-next-line unicorn/prefer-structured-clone -- This captures JSON transport semantics, not a deep clone.
+ return JSON.parse(JSON.stringify({ name: fixture.name, frames })) as unknown;
+}
+
+export function readCommandFixture(path: string): CommandFixture {
+ return JSON.parse(readFileSync(path, "utf8")) as CommandFixture;
+}
diff --git a/packages/typing-fixtures/commands.schema.json b/packages/typing-fixtures/commands.schema.json
new file mode 100644
index 000000000..b8adf517e
--- /dev/null
+++ b/packages/typing-fixtures/commands.schema.json
@@ -0,0 +1,65 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "oxytype:engine-command-fixture:1",
+ "title": "Oxytype deterministic command fixture v1",
+ "type": "object",
+ "required": ["name", "config", "words", "commands"],
+ "additionalProperties": false,
+ "$defs": {
+ "text": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 0, "maximum": 65535 }
+ }
+ },
+ "properties": {
+ "name": { "type": "string", "minLength": 1 },
+ "config": {
+ "type": "object",
+ "description": "Legacy Config projection; omitted fields use frozen oracle defaults. Rust bindings generate normalized config separately."
+ },
+ "words": { "type": "array", "items": { "$ref": "#/$defs/text" } },
+ "commands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["op", "now"],
+ "additionalProperties": false,
+ "properties": {
+ "op": {
+ "enum": [
+ "insert",
+ "delete",
+ "start",
+ "advance",
+ "finish",
+ "end",
+ "abandon",
+ "reset",
+ "observe"
+ ]
+ },
+ "now": { "type": "number" },
+ "date": { "type": "number" },
+ "data": { "$ref": "#/$defs/text" },
+ "kind": { "enum": ["deleteContentBackward", "deleteWordBackward"] },
+ "reason": { "type": "string" },
+ "correctShiftUsed": { "type": ["boolean", "null"] },
+ "automatic": { "const": true },
+ "isCompositionEnding": { "const": true },
+ "eventType": { "enum": ["keydown", "keyup", "composition", "timer"] },
+ "eventData": { "type": "object" }
+ },
+ "allOf": [
+ {
+ "if": { "properties": { "op": { "const": "insert" } } },
+ "then": { "required": ["data"] }
+ },
+ {
+ "if": { "properties": { "op": { "const": "observe" } } },
+ "then": { "required": ["eventType", "eventData"] }
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/typing-fixtures/commands/code-indent.json b/packages/typing-fixtures/commands/code-indent.json
new file mode 100644
index 000000000..ca1b92d25
--- /dev/null
+++ b/packages/typing-fixtures/commands/code-indent.json
@@ -0,0 +1,30 @@
+{
+ "name": "code-indent",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "language": "code_javascript",
+ "codeUnindentOnBackspace": true
+ },
+ "words": [
+ [111, 110, 101, 10],
+ [9, 9, 116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 10],
+ "now": 0
+ },
+ {
+ "op": "delete",
+ "now": 100,
+ "kind": "deleteContentBackward"
+ },
+ {
+ "op": "insert",
+ "data": [10, 111, 110, 101, 10],
+ "now": 200
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/delete-error-letter.json b/packages/typing-fixtures/commands/delete-error-letter.json
new file mode 100644
index 000000000..8b656ef39
--- /dev/null
+++ b/packages/typing-fixtures/commands/delete-error-letter.json
@@ -0,0 +1,34 @@
+{
+ "name": "delete-error-letter",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "deleteOnError": "letter"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32],
+ "now": 100
+ },
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 500
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/delete-error-letter_hard.json b/packages/typing-fixtures/commands/delete-error-letter_hard.json
new file mode 100644
index 000000000..b9171799c
--- /dev/null
+++ b/packages/typing-fixtures/commands/delete-error-letter_hard.json
@@ -0,0 +1,34 @@
+{
+ "name": "delete-error-letter_hard",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "deleteOnError": "letter_hard"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32],
+ "now": 100
+ },
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 500
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/delete-error-word.json b/packages/typing-fixtures/commands/delete-error-word.json
new file mode 100644
index 000000000..d95cbfab7
--- /dev/null
+++ b/packages/typing-fixtures/commands/delete-error-word.json
@@ -0,0 +1,34 @@
+{
+ "name": "delete-error-word",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "deleteOnError": "word"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32],
+ "now": 100
+ },
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 500
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/delete-error-word_hard.json b/packages/typing-fixtures/commands/delete-error-word_hard.json
new file mode 100644
index 000000000..a4cf257ef
--- /dev/null
+++ b/packages/typing-fixtures/commands/delete-error-word_hard.json
@@ -0,0 +1,34 @@
+{
+ "name": "delete-error-word_hard",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "deleteOnError": "word_hard"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32],
+ "now": 100
+ },
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 500
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/finite-edit.json b/packages/typing-fixtures/commands/finite-edit.json
new file mode 100644
index 000000000..f730a4799
--- /dev/null
+++ b/packages/typing-fixtures/commands/finite-edit.json
@@ -0,0 +1,51 @@
+{
+ "name": "finite-edit",
+ "config": {
+ "mode": "words",
+ "words": 2
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "delete",
+ "now": 50,
+ "kind": "deleteContentBackward"
+ },
+ {
+ "op": "insert",
+ "data": [110, 101, 32],
+ "now": 100
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 1100
+ },
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 1200
+ },
+ {
+ "op": "finish",
+ "now": 1300
+ },
+ {
+ "op": "reset",
+ "now": 1400
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32, 116, 119, 111],
+ "now": 1500
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/freedom-regression.json b/packages/typing-fixtures/commands/freedom-regression.json
new file mode 100644
index 000000000..e7312f3b0
--- /dev/null
+++ b/packages/typing-fixtures/commands/freedom-regression.json
@@ -0,0 +1,34 @@
+{
+ "name": "freedom-regression",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "freedomMode": true
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32],
+ "now": 0
+ },
+ {
+ "op": "delete",
+ "now": 100,
+ "kind": "deleteContentBackward"
+ },
+ {
+ "op": "insert",
+ "data": [32],
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [116, 119, 111],
+ "now": 400
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/ime-typography.json b/packages/typing-fixtures/commands/ime-typography.json
new file mode 100644
index 000000000..e8fab4a9d
--- /dev/null
+++ b/packages/typing-fixtures/commands/ime-typography.json
@@ -0,0 +1,25 @@
+{
+ "name": "ime-typography",
+ "config": {
+ "mode": "words",
+ "words": 2
+ },
+ "words": [
+ [97, 32],
+ [8220, 98, 8221, 8212]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [97, 12288],
+ "now": 0,
+ "isCompositionEnding": true
+ },
+ {
+ "op": "insert",
+ "data": [34, 98, 34, 45],
+ "now": 100,
+ "isCompositionEnding": true
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/infinite-words.json b/packages/typing-fixtures/commands/infinite-words.json
new file mode 100644
index 000000000..555289a2b
--- /dev/null
+++ b/packages/typing-fixtures/commands/infinite-words.json
@@ -0,0 +1,26 @@
+{
+ "name": "infinite-words",
+ "config": {
+ "mode": "words",
+ "words": 0
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32, 116, 119, 111, 32],
+ "now": 0
+ },
+ {
+ "op": "advance",
+ "now": 5000
+ },
+ {
+ "op": "abandon",
+ "now": 6000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/master-finish-once.json b/packages/typing-fixtures/commands/master-finish-once.json
new file mode 100644
index 000000000..368469662
--- /dev/null
+++ b/packages/typing-fixtures/commands/master-finish-once.json
@@ -0,0 +1,37 @@
+{
+ "name": "master-finish-once",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "difficulty": "master"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32, 116, 119, 111],
+ "now": 100
+ },
+ {
+ "op": "finish",
+ "now": 100
+ },
+ {
+ "op": "reset",
+ "now": 200
+ },
+ {
+ "op": "insert",
+ "data": [111, 110, 101, 32, 116, 119, 111],
+ "now": 300
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/stop-letter.json b/packages/typing-fixtures/commands/stop-letter.json
new file mode 100644
index 000000000..8c37f5d2d
--- /dev/null
+++ b/packages/typing-fixtures/commands/stop-letter.json
@@ -0,0 +1,24 @@
+{
+ "name": "stop-letter",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "stopOnError": "letter"
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111, 120],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [110, 101, 32, 116, 119, 111],
+ "now": 100
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/timer-stall.json b/packages/typing-fixtures/commands/timer-stall.json
new file mode 100644
index 000000000..e3ece2add
--- /dev/null
+++ b/packages/typing-fixtures/commands/timer-stall.json
@@ -0,0 +1,38 @@
+{
+ "name": "timer-stall",
+ "config": {
+ "mode": "words",
+ "words": 2
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [111],
+ "now": 100
+ },
+ {
+ "op": "advance",
+ "now": 3400
+ },
+ {
+ "op": "advance",
+ "now": 5000
+ },
+ {
+ "op": "finish",
+ "now": 5000
+ },
+ {
+ "op": "reset",
+ "now": 5001
+ },
+ {
+ "op": "advance",
+ "now": 9000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/unicode-halves.json b/packages/typing-fixtures/commands/unicode-halves.json
new file mode 100644
index 000000000..b1487ca4a
--- /dev/null
+++ b/packages/typing-fixtures/commands/unicode-halves.json
@@ -0,0 +1,33 @@
+{
+ "name": "unicode-halves",
+ "config": {
+ "mode": "words",
+ "words": 2
+ },
+ "words": [
+ [55357, 56832, 32],
+ [101, 769]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "data": [55357],
+ "now": 0
+ },
+ {
+ "op": "insert",
+ "data": [56832],
+ "now": 20
+ },
+ {
+ "op": "insert",
+ "data": [32],
+ "now": 40
+ },
+ {
+ "op": "insert",
+ "data": [101, 769],
+ "now": 100
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/code-indent.json b/packages/typing-fixtures/oracle/code-indent.json
new file mode 100644
index 000000000..eb0e3f8e2
--- /dev/null
+++ b/packages/typing-fixtures/oracle/code-indent.json
@@ -0,0 +1,478 @@
+{
+ "name": "code-indent",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [9, 9],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [10],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 10],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [9, 9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 10],
+ [9, 9, 116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111, 110, 101],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [10],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 10],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [9, 9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": []
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101]
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 10],
+ [9, 9, 116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [9, 9, 111, 110, 101, 10],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [10],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 10],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [9, 9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": []
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [10],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 10],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [9],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [9, 9],
+ "lastWord": true,
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [9, 9, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": [9, 9, 111, 110],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 4,
+ "inputValue": [9, 9, 111, 110, 101],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [10],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 5,
+ "inputValue": [9, 9, 111, 110, 101, 10],
+ "commitsWord": true,
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 200,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 10],
+ [9, 9, 116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/delete-error-letter.json b/packages/typing-fixtures/oracle/delete-error-letter.json
new file mode 100644
index 000000000..9e38dd46f
--- /dev/null
+++ b/packages/typing-fixtures/oracle/delete-error-letter.json
@@ -0,0 +1,564 @@
+{
+ "name": "delete-error-letter",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 500,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/delete-error-letter_hard.json b/packages/typing-fixtures/oracle/delete-error-letter_hard.json
new file mode 100644
index 000000000..bbde8bd9c
--- /dev/null
+++ b/packages/typing-fixtures/oracle/delete-error-letter_hard.json
@@ -0,0 +1,640 @@
+{
+ "name": "delete-error-letter_hard",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111, 110, 101],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101],
+ "automatic": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 116]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 4,
+ "inputValue": [111, 110, 101],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 119]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/delete-error-word.json b/packages/typing-fixtures/oracle/delete-error-word.json
new file mode 100644
index 000000000..83b1dda4d
--- /dev/null
+++ b/packages/typing-fixtures/oracle/delete-error-word.json
@@ -0,0 +1,520 @@
+{
+ "name": "delete-error-word",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 500,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/delete-error-word_hard.json b/packages/typing-fixtures/oracle/delete-error-word_hard.json
new file mode 100644
index 000000000..7d772cda0
--- /dev/null
+++ b/packages/typing-fixtures/oracle/delete-error-word_hard.json
@@ -0,0 +1,552 @@
+{
+ "name": "delete-error-word_hard",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [],
+ "automatic": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [120],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [116]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [119]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "deleteWordBackward",
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [],
+ "automatic": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 500,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/finite-edit.json b/packages/typing-fixtures/oracle/finite-edit.json
new file mode 100644
index 000000000..a0905e71d
--- /dev/null
+++ b/packages/typing-fixtures/oracle/finite-edit.json
@@ -0,0 +1,842 @@
+{
+ "name": "finite-edit",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111, 120],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 50,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111]
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 50,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 50,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1100,
+ "data": {
+ "event": "end",
+ "timer": 1,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 50,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1100,
+ "data": {
+ "event": "end",
+ "timer": 1,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 50,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 1100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1100,
+ "data": {
+ "event": "end",
+ "timer": 1,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/freedom-regression.json b/packages/typing-fixtures/oracle/freedom-regression.json
new file mode 100644
index 000000000..c9d944eb6
--- /dev/null
+++ b/packages/typing-fixtures/oracle/freedom-regression.json
@@ -0,0 +1,437 @@
+{
+ "name": "freedom-regression",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111, 110, 101],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101]
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "deleteContentBackward",
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 200,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 400,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 400,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 400,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 400,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/ime-typography.json b/packages/typing-fixtures/oracle/ime-typography.json
new file mode 100644
index 000000000..c4eb8fd07
--- /dev/null
+++ b/packages/typing-fixtures/oracle/ime-typography.json
@@ -0,0 +1,190 @@
+{
+ "name": "ime-typography",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97],
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 32],
+ "commitsWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [97, 32],
+ [8220, 98, 8221, 8212]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [8220, 98, 8221, 8212],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97],
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 32],
+ "commitsWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [8220],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [8220],
+ "lastWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [98],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [8220, 98],
+ "lastWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [8221],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [8220, 98, 8221],
+ "lastWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [8212],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": [8220, 98, 8221, 8212],
+ "lastWord": true,
+ "isCompositionEnding": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 100,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [97, 32],
+ [8220, 98, 8221, 8212]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/infinite-words.json b/packages/typing-fixtures/oracle/infinite-words.json
new file mode 100644
index 000000000..dc5ee3380
--- /dev/null
+++ b/packages/typing-fixtures/oracle/infinite-words.json
@@ -0,0 +1,509 @@
+{
+ "name": "infinite-words",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111, 32],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": [116, 119, 111, 32],
+ "commitsWord": true,
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "words",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111, 32],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": [116, 119, 111, 32],
+ "commitsWord": true,
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 4000
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "words",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111, 32],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 3,
+ "inputValue": [116, 119, 111, 32],
+ "commitsWord": true,
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 4000
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6000,
+ "data": {
+ "event": "end",
+ "timer": 6,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "words",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/master-finish-once.json b/packages/typing-fixtures/oracle/master-finish-once.json
new file mode 100644
index 000000000..68c44c5a6
--- /dev/null
+++ b/packages/typing-fixtures/oracle/master-finish-once.json
@@ -0,0 +1,323 @@
+{
+ "name": "master-finish-once",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [120],
+ "active": false,
+ "ended": true,
+ "reason": "difficulty"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [120],
+ "active": false,
+ "ended": true,
+ "reason": "difficulty"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [120],
+ "active": false,
+ "ended": true,
+ "reason": "difficulty"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/stop-letter.json b/packages/typing-fixtures/oracle/stop-letter.json
new file mode 100644
index 000000000..cbb66b562
--- /dev/null
+++ b/packages/typing-fixtures/oracle/stop-letter.json
@@ -0,0 +1,206 @@
+{
+ "name": "stop-letter",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111],
+ "inputStopped": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [116, 119, 111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111],
+ "inputStopped": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [116],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [116],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [119],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [116, 119],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 2,
+ "inputValue": [116, 119, 111],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 100,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/timer-stall.json b/packages/typing-fixtures/oracle/timer-stall.json
new file mode 100644
index 000000000..1c9d457d5
--- /dev/null
+++ b/packages/typing-fixtures/oracle/timer-stall.json
@@ -0,0 +1,339 @@
+{
+ "name": "timer-stall",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 2300
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 2300
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4900,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 900
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 3300,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "drift": 2300
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4900,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "drift": 900
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 4900,
+ "data": {
+ "event": "end",
+ "timer": 4,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/unicode-halves.json b/packages/typing-fixtures/oracle/unicode-halves.json
new file mode 100644
index 000000000..04ffff189
--- /dev/null
+++ b/packages/typing-fixtures/oracle/unicode-halves.json
@@ -0,0 +1,282 @@
+{
+ "name": "unicode-halves",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [55357],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [55357],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [55357]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [55357, 56832, 32],
+ [101, 769]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [55357, 56832],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [55357],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [55357]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 20,
+ "data": {
+ "inputType": "insertText",
+ "data": [56832],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [55357, 56832]
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [55357, 56832, 32],
+ [101, 769]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [55357],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [55357]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 20,
+ "data": {
+ "inputType": "insertText",
+ "data": [56832],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [55357, 56832]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 40,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [55357, 56832, 32],
+ "commitsWord": true
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [55357, 56832, 32],
+ [101, 769]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 1,
+ "inputValue": [101, 769],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [55357],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [55357]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 20,
+ "data": {
+ "inputType": "insertText",
+ "data": [56832],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [55357, 56832]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 40,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [55357, 56832, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [101],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 100,
+ "data": {
+ "inputType": "insertText",
+ "data": [769],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [101, 769],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 100,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [55357, 56832, 32],
+ [101, 769]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
From 259adfe849c1263f62bf06400bb02ca078e41f25 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:00:43 +0530
Subject: [PATCH 05/93] feat(engine): add pinned Rust workspace and finite
UTF-16 session slice
---
.gitignore | 4 +
Cargo.lock | 363 ++++++++++++
Cargo.toml | 28 +
crates/typing-engine-wasm/Cargo.toml | 20 +
crates/typing-engine-wasm/src/lib.rs | 62 +++
crates/typing-engine/Cargo.toml | 15 +
.../typing-engine/src/bin/export-bindings.rs | 16 +
.../typing-engine/src/bin/fixture-runner.rs | 26 +
crates/typing-engine/src/lib.rs | 25 +
crates/typing-engine/src/session.rs | 521 ++++++++++++++++++
crates/typing-engine/src/text.rs | 79 +++
crates/typing-engine/tests/slice.rs | 96 ++++
package.json | 7 +-
rust-toolchain.toml | 5 +
scripts/engine.ts | 85 +++
scripts/tsconfig.json | 10 +
16 files changed, 1361 insertions(+), 1 deletion(-)
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 crates/typing-engine-wasm/Cargo.toml
create mode 100644 crates/typing-engine-wasm/src/lib.rs
create mode 100644 crates/typing-engine/Cargo.toml
create mode 100644 crates/typing-engine/src/bin/export-bindings.rs
create mode 100644 crates/typing-engine/src/bin/fixture-runner.rs
create mode 100644 crates/typing-engine/src/lib.rs
create mode 100644 crates/typing-engine/src/session.rs
create mode 100644 crates/typing-engine/src/text.rs
create mode 100644 crates/typing-engine/tests/slice.rs
create mode 100644 rust-toolchain.toml
create mode 100644 scripts/engine.ts
create mode 100644 scripts/tsconfig.json
diff --git a/.gitignore b/.gitignore
index 3b04072dd..2f6a763df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -138,3 +138,7 @@ frontend/playwright-report/
# TanStack Router generator scratch files
.tanstack/
+
+# Rust / generated Wasm and type bindings
+/target/
+/packages/typing-engine/generated/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 000000000..c1c7beee1
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,363 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
+dependencies = [
+ "foldhash",
+ "indexmap",
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "ts-rs"
+version = "12.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
+dependencies = [
+ "serde_json",
+ "thiserror",
+ "ts-rs-macros",
+]
+
+[[package]]
+name = "ts-rs-macros"
+version = "12.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "termcolor",
+]
+
+[[package]]
+name = "typing-engine"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "ts-rs",
+]
+
+[[package]]
+name = "typing-engine-wasm"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "typing-engine",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 000000000..a9ce7534a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,28 @@
+[workspace]
+members = ["crates/typing-engine", "crates/typing-engine-wasm"]
+resolver = "3"
+
+[workspace.package]
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.99"
+license = "GPL-3.0-only"
+repository = "https://github.com/voltcrash/oxytype"
+
+[workspace.dependencies]
+serde = { version = "1.0.229", features = ["derive"] }
+serde_json = { version = "1.0.151", features = ["preserve_order"] }
+wasm-bindgen = "=0.2.129"
+ts-rs = { version = "=12.0.1", features = ["serde-json-impl"] }
+
+[workspace.lints.rust]
+unsafe_code = "forbid"
+
+[workspace.lints.clippy]
+all = "warn"
+
+[profile.release]
+lto = true
+codegen-units = 1
+opt-level = "s"
+strip = "debuginfo"
diff --git a/crates/typing-engine-wasm/Cargo.toml b/crates/typing-engine-wasm/Cargo.toml
new file mode 100644
index 000000000..830578b23
--- /dev/null
+++ b/crates/typing-engine-wasm/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "typing-engine-wasm"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+typing-engine = { path = "../typing-engine" }
+serde_json.workspace = true
+wasm-bindgen.workspace = true
+serde.workspace = true
+serde-wasm-bindgen = "0.6.5"
+
+[lints]
+workspace = true
diff --git a/crates/typing-engine-wasm/src/lib.rs b/crates/typing-engine-wasm/src/lib.rs
new file mode 100644
index 000000000..e018cc752
--- /dev/null
+++ b/crates/typing-engine-wasm/src/lib.rs
@@ -0,0 +1,62 @@
+use serde::Serialize;
+use typing_engine::{BINDING_VERSION, Config, Session, Text};
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn binding_version() -> u32 {
+ BINDING_VERSION
+}
+
+/// Thin serialization/ownership boundary. All decisions live in typing-engine.
+#[wasm_bindgen]
+pub struct WasmSession {
+ session: Option,
+}
+
+#[wasm_bindgen]
+impl WasmSession {
+ #[wasm_bindgen(constructor)]
+ pub fn new(config: &str, words: &str, version: u32) -> Result {
+ if version != BINDING_VERSION {
+ return Err(JsError::new("Binding version mismatch"));
+ }
+ let config: Config = serde_json::from_str(config)?;
+ let words: Vec = serde_json::from_str(words)?;
+ Ok(Self {
+ session: Some(Session::new(config, words).map_err(|message| JsError::new(&message))?),
+ })
+ }
+ pub fn command(&mut self, command: &str) -> Result {
+ let command = serde_json::from_str(command)?;
+ let session = self
+ .session
+ .as_mut()
+ .ok_or_else(|| JsError::new("Session disposed"))?;
+ let update = session
+ .apply(command)
+ .map_err(|message| JsError::new(&message))?;
+ Ok(serde_json::to_string(&update)?)
+ }
+ pub fn log(&self) -> Result {
+ let session = self
+ .session
+ .as_ref()
+ .ok_or_else(|| JsError::new("Session disposed"))?;
+ Ok(serde_json::to_string(&session.log())?)
+ }
+ pub fn command_value(&mut self, command: JsValue) -> Result {
+ let command = serde_wasm_bindgen::from_value(command)?;
+ let session = self
+ .session
+ .as_mut()
+ .ok_or_else(|| JsError::new("Session disposed"))?;
+ let update = session
+ .apply(command)
+ .map_err(|message| JsError::new(&message))?;
+ Ok(update
+ .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))?)
+ }
+ pub fn dispose(&mut self) {
+ self.session = None;
+ }
+}
diff --git a/crates/typing-engine/Cargo.toml b/crates/typing-engine/Cargo.toml
new file mode 100644
index 000000000..363ba8801
--- /dev/null
+++ b/crates/typing-engine/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "typing-engine"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
+ts-rs.workspace = true
+
+[lints]
+workspace = true
diff --git a/crates/typing-engine/src/bin/export-bindings.rs b/crates/typing-engine/src/bin/export-bindings.rs
new file mode 100644
index 000000000..4c00e38d1
--- /dev/null
+++ b/crates/typing-engine/src/bin/export-bindings.rs
@@ -0,0 +1,16 @@
+use ts_rs::TS;
+use typing_engine::{
+ Command, Config,
+ session::{State, Update},
+};
+
+fn main() -> Result<(), Box> {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../packages/typing-engine/generated/types");
+ let config = ts_rs::Config::new().with_out_dir(root);
+ Command::export_all(&config)?;
+ Config::export_all(&config)?;
+ State::export_all(&config)?;
+ Update::export_all(&config)?;
+ Ok(())
+}
diff --git a/crates/typing-engine/src/bin/fixture-runner.rs b/crates/typing-engine/src/bin/fixture-runner.rs
new file mode 100644
index 000000000..23ab2b890
--- /dev/null
+++ b/crates/typing-engine/src/bin/fixture-runner.rs
@@ -0,0 +1,26 @@
+use serde::Deserialize;
+use serde_json::json;
+use std::io::{self, Read};
+use typing_engine::{Command, Config, Session, Text};
+
+#[derive(Deserialize)]
+struct Fixture {
+ name: String,
+ config: Config,
+ words: Vec,
+ commands: Vec,
+}
+
+fn main() -> Result<(), Box> {
+ let mut input = String::new();
+ io::stdin().read_to_string(&mut input)?;
+ let fixture: Fixture = serde_json::from_str(&input)?;
+ let mut session = Session::new(fixture.config, fixture.words)?;
+ let mut frames = vec![];
+ for command in fixture.commands {
+ let update = session.apply(command)?;
+ frames.push(json!({"state":update.state,"log":session.log()}));
+ }
+ println!("{}", json!({"name":fixture.name,"frames":frames}));
+ Ok(())
+}
diff --git a/crates/typing-engine/src/lib.rs b/crates/typing-engine/src/lib.rs
new file mode 100644
index 000000000..e9ff18836
--- /dev/null
+++ b/crates/typing-engine/src/lib.rs
@@ -0,0 +1,25 @@
+//! Platform-independent typing rules. Hosts provide content, observations and time.
+pub mod session;
+pub mod text;
+
+pub use session::{Command, Config, Session};
+pub use text::Text;
+
+pub const BINDING_VERSION: u32 = 1;
+pub const BEHAVIOR_VERSION: u32 = 1;
+pub const EVENT_LOG_VERSION: u32 = 1;
+
+/// Match JavaScript Math.round: halfway values round toward positive infinity.
+pub fn js_round(value: f64) -> f64 {
+ let floor = value.floor();
+ if value - floor < 0.5 {
+ floor
+ } else {
+ floor + 1.0
+ }
+}
+
+/// Existing roundTo2 uses Number.EPSILON before rounding.
+pub fn round2(value: f64) -> f64 {
+ js_round((value + f64::EPSILON) * 100.0) / 100.0
+}
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
new file mode 100644
index 000000000..7dc2ac9c6
--- /dev/null
+++ b/crates/typing-engine/src/session.rs
@@ -0,0 +1,521 @@
+use crate::{
+ EVENT_LOG_VERSION, Text, round2,
+ text::{delete_word, is_space, normalize},
+};
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+use ts_rs::TS;
+
+#[derive(Clone, Debug, Deserialize, Serialize, TS)]
+#[serde(default, rename_all = "camelCase")]
+pub struct Config {
+ pub mode: String,
+ pub words: u32,
+ pub time: f64,
+ pub language: String,
+ pub difficulty: String,
+ pub strict_space: bool,
+ pub stop_on_error: String,
+ pub delete_on_error: String,
+ pub quick_end: bool,
+ pub freedom_mode: bool,
+ pub confidence_mode: String,
+ pub blind_mode: bool,
+ pub code_unindent_on_backspace: bool,
+ pub funbox: Vec,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ mode: "time".into(),
+ words: 50,
+ time: 30.0,
+ language: "english".into(),
+ difficulty: "normal".into(),
+ strict_space: false,
+ stop_on_error: "off".into(),
+ delete_on_error: "off".into(),
+ quick_end: false,
+ freedom_mode: false,
+ confidence_mode: "off".into(),
+ blind_mode: false,
+ code_unindent_on_backspace: false,
+ funbox: vec![],
+ }
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, TS)]
+#[serde(tag = "op", rename_all = "camelCase")]
+pub enum Command {
+ #[serde(rename_all = "camelCase")]
+ Insert {
+ data: Text,
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ #[serde(default)]
+ correct_shift_used: Option,
+ #[serde(default)]
+ automatic: Option,
+ #[serde(default)]
+ is_composition_ending: Option,
+ },
+ Delete {
+ kind: String,
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ },
+ Start {
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ },
+ Advance {
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ },
+ Finish {
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ #[serde(default)]
+ reason: Option,
+ },
+ End {
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ },
+ Abandon {
+ now: f64,
+ #[serde(default)]
+ date: f64,
+ },
+ Reset {
+ #[serde(default)]
+ now: f64,
+ },
+ #[serde(rename_all = "camelCase")]
+ Observe {
+ now: f64,
+ event_type: String,
+ event_data: Value,
+ },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+pub struct State {
+ pub word_index: usize,
+ pub input_value: Text,
+ pub active: bool,
+ pub ended: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ #[ts(optional)]
+ pub reason: Option,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+pub struct Event {
+ #[serde(rename = "type")]
+ pub kind: String,
+ pub test_ms: f64,
+ pub data: Value,
+}
+
+#[derive(Serialize, TS)]
+pub struct Update {
+ pub state: State,
+ pub events: Vec,
+}
+
+#[derive(Clone)]
+struct Recorded {
+ kind: String,
+ now: f64,
+ data: Value,
+}
+
+pub struct Session {
+ config: Config,
+ words: Vec,
+ input: Vec,
+ index: usize,
+ active: bool,
+ ended: bool,
+ reason: Option,
+ start: Option,
+ ticks: u32,
+ events: Vec,
+}
+
+impl Session {
+ pub fn new(config: Config, words: Vec) -> Result {
+ if !["time", "words", "quote", "custom", "zen"].contains(&config.mode.as_str()) {
+ return Err("Invalid mode".into());
+ }
+ if !config.time.is_finite() || config.time < 0.0 {
+ return Err("Invalid time limit".into());
+ }
+ Ok(Self {
+ config,
+ input: vec![Text::default(); words.len().max(1)],
+ words,
+ index: 0,
+ active: false,
+ ended: false,
+ reason: None,
+ start: None,
+ ticks: 0,
+ events: vec![],
+ })
+ }
+
+ pub fn state(&self) -> State {
+ State {
+ word_index: self.index,
+ input_value: self.current(),
+ active: self.active,
+ ended: self.ended,
+ reason: self.reason.clone(),
+ }
+ }
+ fn current(&self) -> Text {
+ self.input.get(self.index).cloned().unwrap_or_default()
+ }
+ fn target(&self) -> Text {
+ self.words.get(self.index).cloned().unwrap_or_default()
+ }
+ fn nospace(&self) -> bool {
+ self.config.funbox.iter().any(|name| {
+ ["nospace", "arrows", "morse", "underscore_spaces"].contains(&name.as_str())
+ })
+ }
+ fn record(&mut self, kind: &str, now: f64, data: Value) {
+ self.events.push(Recorded {
+ kind: kind.into(),
+ now: round2(now),
+ data,
+ });
+ }
+ fn start(&mut self, now: f64, date: f64) {
+ if self.active || self.ended {
+ return;
+ }
+ self.active = true;
+ self.start = Some(now);
+ self.record(
+ "timer",
+ now,
+ json!({"event":"start", "timer":0, "date":date}),
+ );
+ }
+ fn end(&mut self, now: f64, date: f64, reason: Option) {
+ if !self.active || self.ended {
+ return;
+ }
+ self.active = false;
+ self.ended = true;
+ self.reason = reason;
+ self.record("timer", now, json!({"event":"end", "timer":((now-self.start.unwrap_or(now))/1000.0).floor(), "date":date}));
+ }
+
+ pub fn apply(&mut self, command: Command) -> Result {
+ let now = match &command {
+ Command::Insert { now, .. }
+ | Command::Delete { now, .. }
+ | Command::Start { now, .. }
+ | Command::Advance { now, .. }
+ | Command::Finish { now, .. }
+ | Command::End { now, .. }
+ | Command::Abandon { now, .. }
+ | Command::Reset { now }
+ | Command::Observe { now, .. } => *now,
+ };
+ if !now.is_finite() || now.abs() > 9_007_199_254_740_991.0 {
+ return Err("Invalid timestamp".into());
+ }
+ let before = self.events.len();
+ match command {
+ Command::Insert {
+ data,
+ now,
+ date,
+ correct_shift_used,
+ automatic,
+ is_composition_ending,
+ } => {
+ for unit in data.0 {
+ if self.ended {
+ break;
+ }
+ self.insert(
+ unit,
+ now,
+ date,
+ correct_shift_used,
+ automatic == Some(true),
+ is_composition_ending == Some(true),
+ );
+ }
+ }
+ Command::Delete { kind, now, .. } => {
+ if !["deleteContentBackward", "deleteWordBackward"].contains(&kind.as_str()) {
+ return Err("Invalid deletion kind".into());
+ }
+ self.delete(&kind, now);
+ }
+ Command::Start { now, date } => self.start(now, date),
+ Command::Finish { now, date, reason } => self.end(now, date, reason),
+ Command::End { now, date } | Command::Abandon { now, date } => {
+ self.end(now, date, None)
+ }
+ Command::Advance { now, date } => self.advance(now, date),
+ Command::Reset { .. } => {
+ self.index = 0;
+ self.active = false;
+ self.ended = false;
+ self.reason = None;
+ self.start = None;
+ self.ticks = 0;
+ self.events.clear();
+ self.input = vec![Text::default(); self.words.len().max(1)];
+ }
+ Command::Observe { .. } => {
+ return Err("Observations not yet implemented in proof slice".into());
+ }
+ }
+ let origin = self.origin();
+ let events = self
+ .events
+ .get(before..)
+ .unwrap_or_default()
+ .iter()
+ .map(|event| Self::event(event, origin))
+ .collect();
+ Ok(Update {
+ state: self.state(),
+ events,
+ })
+ }
+
+ fn insert(
+ &mut self,
+ unit: u16,
+ now: f64,
+ date: f64,
+ shift: Option,
+ automatic: bool,
+ composition: bool,
+ ) {
+ let input = self.current();
+ let target = self.target();
+ let nospace = self.nospace();
+ if unit == 10
+ && self.config.mode != "zen"
+ && !self.words.iter().any(|word| word.0.contains(&10))
+ {
+ return;
+ }
+ if is_space(unit) && nospace {
+ return;
+ }
+ let unit = normalize(
+ unit,
+ target.0.get(input.len()).copied(),
+ &self.config.language,
+ );
+ let commit = is_space(unit) || unit == 10 || (nospace && input.len() + 1 == target.len());
+ if is_space(unit)
+ && input.is_empty()
+ && !self.config.strict_space
+ && self.config.difficulty == "normal"
+ && !self.config.delete_on_error.contains("hard")
+ {
+ return;
+ }
+ let mut next = input.clone();
+ next.0.push(unit);
+ let word_correct = next == target;
+ let advance = commit
+ && (self.config.mode == "zen"
+ || (!(input.is_empty()
+ && !nospace
+ && (self.config.strict_space || self.config.difficulty != "normal"))
+ && (word_correct
+ || (self.config.stop_on_error == "off"
+ && self.config.delete_on_error == "off"))));
+ let limit = if self.config.mode == "zen" {
+ 30
+ } else {
+ target.len() + 20
+ };
+ if input.len() >= limit && !advance {
+ return;
+ }
+ self.start(now, date);
+ let correct = self.config.mode == "zen"
+ || (shift != Some(false) && target.0.get(input.len()) == Some(&unit));
+ let stopped = (self.config.stop_on_error == "letter" && !correct) || shift == Some(false);
+ if stopped {
+ next = input.clone();
+ }
+ let advance = advance && !stopped;
+ let mut data = json!({"inputType":"insertText", "data":Text(vec![unit]), "correct":correct, "wordIndex":self.index,
+ "charIndex":input.len(), "inputValue":next});
+ for (key, yes) in [
+ ("inputStopped", stopped),
+ ("commitsWord", advance),
+ ("lastWord", self.index + 1 == self.words.len()),
+ ("automatic", automatic),
+ ("isCompositionEnding", composition),
+ ] {
+ if yes {
+ data[key] = json!(true);
+ }
+ }
+ self.input
+ .resize(self.input.len().max(self.index + 1), Text::default());
+ self.input[self.index] = next;
+ self.record("input", now, data);
+ // Full input/lifecycle port adds difficulty/deletion penalties after browser proof.
+ let finite = self.config.mode != "time"
+ && self.config.mode != "zen"
+ && !(self.config.mode == "words" && self.config.words == 0);
+ let quick = self.config.quick_end
+ && target.len() == input.len() + 1
+ && self.config.stop_on_error == "off"
+ && self.config.delete_on_error == "off";
+ if self.index + 1 >= self.words.len() && finite && (word_correct || quick || advance) {
+ self.end(now, date, None);
+ } else if advance && (self.index + 1 < self.words.len() || self.config.mode == "zen") {
+ self.index += 1;
+ }
+ }
+
+ fn delete(&mut self, kind: &str, now: f64) {
+ if !self.active || self.ended {
+ return;
+ }
+ let input = self.current();
+ if input.is_empty() && self.index == 0 {
+ return;
+ }
+ let prev = self
+ .index
+ .checked_sub(1)
+ .and_then(|index| self.input.get(index))
+ .cloned()
+ .unwrap_or_default();
+ let previous_correct = self.index > 0 && self.words.get(self.index - 1) == Some(&prev);
+ if !self.config.freedom_mode
+ && (self.config.confidence_mode == "max"
+ || (input.is_empty() && (self.config.confidence_mode == "on" || previous_correct)))
+ {
+ return;
+ }
+ let next = if input.is_empty() {
+ self.index -= 1;
+ let mut previous = prev;
+ if kind == "deleteWordBackward" {
+ previous.0.clear();
+ } else if self.nospace() || matches!(previous.0.last(), Some(32 | 10)) {
+ previous.0.pop();
+ }
+ previous
+ } else if kind == "deleteWordBackward" {
+ delete_word(&input)
+ } else {
+ let mut next = input.clone();
+ next.0.pop();
+ next
+ };
+ let char_index = if input.is_empty() {
+ next.len()
+ } else {
+ input.len()
+ };
+ self.record("input",now,json!({"inputType":kind,"wordIndex":self.index,"charIndex":char_index,"inputValue":next}));
+ self.input[self.index] = next;
+ }
+
+ fn advance(&mut self, now: f64, date: f64) {
+ if !self.active {
+ return;
+ }
+ let start = self.start.unwrap_or(now);
+ let drift = round2(now - (start + f64::from(self.ticks + 1) * 1000.0));
+ if drift < 0.0 {
+ return;
+ }
+ let due = ((now - start) / 1000.0).floor() as u32;
+ while self.active && self.ticks < due {
+ self.ticks += 1;
+ let mut data = json!({"event":"step","timer":self.ticks});
+ if self.ticks < due {
+ data["catchup"] = json!(true);
+ } else {
+ data["drift"] = json!(drift);
+ }
+ self.record("timer", now, data);
+ if self.config.mode == "time"
+ && self.config.time > 0.0
+ && f64::from(self.ticks) >= self.config.time
+ {
+ self.end(now, date, None);
+ }
+ }
+ }
+
+ fn rank(kind: &str) -> u8 {
+ match kind {
+ "keyup" => 0,
+ "keydown" => 1,
+ "input" => 2,
+ "composition" => 3,
+ _ => 4,
+ }
+ }
+ fn origin(&self) -> f64 {
+ self.events
+ .iter()
+ .find(|e| e.kind == "timer" && e.data["event"] == "start")
+ .map(|e| e.now)
+ .unwrap_or_else(|| {
+ self.events
+ .iter()
+ .map(|e| e.now)
+ .reduce(f64::min)
+ .unwrap_or(0.0)
+ })
+ }
+ fn event(event: &Recorded, origin: f64) -> Event {
+ Event {
+ kind: event.kind.clone(),
+ test_ms: round2(event.now - origin),
+ data: event.data.clone(),
+ }
+ }
+ pub fn log(&self) -> Value {
+ let mut events = self.events.clone();
+ events.sort_by(|a, b| {
+ a.now
+ .total_cmp(&b.now)
+ .then(Self::rank(&a.kind).cmp(&Self::rank(&b.kind)))
+ });
+ let mode2 = match self.config.mode.as_str() {
+ "words" => self.config.words.to_string(),
+ "time" => self.config.time.to_string(),
+ "quote" => "-1".into(),
+ other => other.into(),
+ };
+ let korean=self.words.iter().flat_map(|word| &word.0).any(|c| matches!(*c,0xac00..=0xd7af | 0x1100..=0x11ff | 0x3130..=0x318f | 0xa960..=0xa97f | 0xd7b0..=0xd7ff));
+ json!({"version":EVENT_LOG_VERSION,"events":events.iter().map(|event|Self::event(event,self.origin())).collect::>(),
+ "context":{"targetWords":self.words,"mode":self.config.mode,"mode2":mode2,"koreanStatus":korean,"bailedOut":false,"isFunboxWithNospacePropertyActive":self.nospace()}})
+ }
+}
diff --git a/crates/typing-engine/src/text.rs b/crates/typing-engine/src/text.rs
new file mode 100644
index 000000000..f1daa7f2d
--- /dev/null
+++ b/crates/typing-engine/src/text.rs
@@ -0,0 +1,79 @@
+use serde::{Deserialize, Serialize};
+use ts_rs::TS;
+
+/// UTF-16 units, not Unicode scalars, UTF-8 bytes, graphemes or terminal columns.
+#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, TS)]
+pub struct Text(pub Vec);
+
+impl From<&str> for Text {
+ fn from(value: &str) -> Self {
+ Self(value.encode_utf16().collect())
+ }
+}
+
+impl Text {
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+ /// Only for native display; never use this lossy conversion for engine rules.
+ pub fn display_lossy(&self) -> String {
+ String::from_utf16_lossy(&self.0)
+ }
+}
+
+pub fn is_space(unit: u16) -> bool {
+ matches!(
+ unit,
+ 0x0020
+ | 0x2002
+ | 0x2003
+ | 0x2009
+ | 0x3000
+ | 0x00a0
+ | 0x1680
+ | 0x202f
+ | 0xfeff
+ | 0x2007
+ | 0x2008
+ | 0x2004
+ | 0x200a
+ | 0x200b
+ )
+}
+
+pub fn normalize(unit: u16, target: Option, language: &str) -> u16 {
+ if let Some(target) = target {
+ let equivalent = unit == target
+ || ((unit == 32 || target == 32) && is_space(unit) && is_space(target))
+ || ["’‘'ʼ׳ʻ᾽", "\"”“„", "–—-‐‑", ",‚"].iter().any(|set| {
+ set.encode_utf16().any(|c| c == unit) && set.encode_utf16().any(|c| c == target)
+ })
+ || (language.split('_').next() == Some("russian")
+ && "ёеe".encode_utf16().any(|c| c == unit)
+ && "ёеe".encode_utf16().any(|c| c == target));
+ if equivalent {
+ return target;
+ }
+ }
+ if is_space(unit) { 32 } else { unit }
+}
+
+/// JavaScript \s, distinct from the app's directly typable space set.
+pub fn js_whitespace(unit: u16) -> bool {
+ matches!(unit, 9..=13 | 32 | 160 | 0x1680 | 0x2000..=0x200a | 0x2028 |
+ 0x2029 | 0x202f | 0x205f | 0x3000 | 0xfeff)
+}
+
+pub fn delete_word(input: &Text) -> Text {
+ let mut end = input.len();
+ while end > 0 && js_whitespace(input.0[end - 1]) {
+ end -= 1;
+ }
+ while end > 0 && !js_whitespace(input.0[end - 1]) {
+ end -= 1;
+ }
+ Text(input.0[..end].to_vec())
+}
diff --git a/crates/typing-engine/tests/slice.rs b/crates/typing-engine/tests/slice.rs
new file mode 100644
index 000000000..ac586c88f
--- /dev/null
+++ b/crates/typing-engine/tests/slice.rs
@@ -0,0 +1,96 @@
+use serde::Deserialize;
+use serde_json::{Value, json};
+use typing_engine::{Command, Config, Session, Text};
+
+#[derive(Deserialize)]
+struct Fixture {
+ name: String,
+ config: Config,
+ words: Vec,
+ commands: Vec,
+}
+
+fn equivalent(a: &Value, b: &Value) -> bool {
+ match (a, b) {
+ (Value::Number(a), Value::Number(b)) => a.as_f64() == b.as_f64(),
+ (Value::Array(a), Value::Array(b)) => {
+ a.len() == b.len() && a.iter().zip(b).all(|(a, b)| equivalent(a, b))
+ }
+ (Value::Object(a), Value::Object(b)) => {
+ a.len() == b.len()
+ && a.iter()
+ .all(|(key, a)| b.get(key).is_some_and(|b| equivalent(a, b)))
+ }
+ _ => a == b,
+ }
+}
+
+#[test]
+fn finite_slice_matches_frozen_intermediate_oracle() {
+ let root =
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packages/typing-fixtures");
+ for name in [
+ "finite-edit",
+ "unicode-halves",
+ "freedom-regression",
+ "ime-typography",
+ "timer-stall",
+ "stop-letter",
+ "infinite-words",
+ ] {
+ let fixture: Fixture = serde_json::from_slice(
+ &std::fs::read(root.join(format!("commands/{name}.json"))).unwrap(),
+ )
+ .unwrap();
+ let expected: Value = serde_json::from_slice(
+ &std::fs::read(root.join(format!("oracle/{name}.json"))).unwrap(),
+ )
+ .unwrap();
+ let mut session = Session::new(fixture.config, fixture.words).unwrap();
+ let mut frames = vec![];
+ for command in fixture.commands {
+ let update = session.apply(command).unwrap();
+ frames.push(json!({"state":update.state,"log":session.log()}));
+ }
+ let actual = json!({"name":fixture.name,"frames":frames});
+ assert!(
+ equivalent(&actual, &expected),
+ "{name}\nactual={actual}\nexpected={expected}"
+ );
+ }
+}
+
+#[test]
+fn independent_sessions_reset_without_cross_contamination() {
+ let config = Config {
+ mode: "words".into(),
+ words: 1,
+ ..Config::default()
+ };
+ let mut a = Session::new(config.clone(), vec![Text::from("a")]).unwrap();
+ let b = Session::new(config, vec![Text::from("b")]).unwrap();
+ a.apply(serde_json::from_value(json!({"op":"insert","data":[97],"now":0})).unwrap())
+ .unwrap();
+ assert!(a.state().ended);
+ assert!(!b.state().active);
+ assert!(b.state().input_value.is_empty());
+ a.apply(Command::Reset { now: 1.0 }).unwrap();
+ assert!(!a.state().ended);
+ assert!(a.state().input_value.is_empty());
+}
+
+#[test]
+fn invalid_command_leaves_state_unchanged() {
+ let mut session = Session::new(Config::default(), vec![]).unwrap();
+ let before = session.log();
+ assert!(
+ session
+ .apply(Command::Start {
+ now: f64::NAN,
+ date: 0.0
+ })
+ .is_err()
+ );
+ assert_eq!(session.log(), before);
+ assert_eq!(Text(vec![0xd83d]).0, vec![0xd83d]);
+}
diff --git a/package.json b/package.json
index e604ac111..70edf2047 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,12 @@
"check-assets-others": "vp run --filter @oxytype/frontend check-assets others",
"check-assets-json": "vp run --filter @oxytype/frontend check-assets json",
"knip": "knip",
- "oxlint": "vp lint"
+ "oxlint": "vp lint",
+ "engine:build": "node scripts/engine.ts build",
+ "engine:test": "node scripts/engine.ts test",
+ "engine:lint": "node scripts/engine.ts lint",
+ "engine:fmt": "node scripts/engine.ts fmt",
+ "engine:fmt-fix": "node scripts/engine.ts fmt-fix"
},
"devDependencies": {
"@commitlint/cli": "21.2.3",
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 000000000..4a36f816b
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,5 @@
+[toolchain]
+channel = "1.99.1"
+profile = "minimal"
+components = ["clippy", "rustfmt"]
+targets = ["wasm32-unknown-unknown"]
diff --git a/scripts/engine.ts b/scripts/engine.ts
new file mode 100644
index 000000000..9de06d2a2
--- /dev/null
+++ b/scripts/engine.ts
@@ -0,0 +1,85 @@
+import { spawnSync } from "node:child_process";
+import { mkdirSync } from "node:fs";
+import { resolve } from "node:path";
+
+const root = resolve(import.meta.dirname, "..");
+const output = resolve(root, "packages/typing-engine/generated");
+function run(command: string, args: string[]): void {
+ const result = spawnSync(command, args, { cwd: root, stdio: "inherit" });
+ if (result.error !== undefined) throw result.error;
+ if (result.status !== 0) process.exit(result.status ?? 1);
+}
+
+switch (process.argv[2]) {
+ case "build": {
+ const bindingVersion = "0.2.129";
+ const installed = spawnSync("wasm-bindgen", ["--version"], {
+ encoding: "utf8",
+ });
+ if (installed.stdout?.trim() !== `wasm-bindgen ${bindingVersion}`) {
+ run("cargo", [
+ "install",
+ "wasm-bindgen-cli",
+ "--version",
+ bindingVersion,
+ "--locked",
+ ]);
+ }
+ run("cargo", [
+ "build",
+ "--locked",
+ "--release",
+ "--target",
+ "wasm32-unknown-unknown",
+ "-p",
+ "typing-engine-wasm",
+ ]);
+ mkdirSync(output, { recursive: true });
+ run("wasm-bindgen", [
+ "--target",
+ "web",
+ "--out-dir",
+ output,
+ "--out-name",
+ "engine",
+ resolve(
+ root,
+ "target/wasm32-unknown-unknown/release/typing_engine_wasm.wasm",
+ ),
+ ]);
+ run("cargo", [
+ "run",
+ "--locked",
+ "--quiet",
+ "-p",
+ "typing-engine",
+ "--bin",
+ "export-bindings",
+ ]);
+ break;
+ }
+ case "test":
+ run("cargo", ["test", "--locked", "--workspace"]);
+ break;
+ case "lint":
+ run("cargo", [
+ "clippy",
+ "--locked",
+ "--workspace",
+ "--all-targets",
+ "--",
+ "-D",
+ "warnings",
+ ]);
+ break;
+ case "fmt":
+ run("cargo", ["fmt", "--all", "--check"]);
+ break;
+ case "fmt-fix":
+ run("cargo", ["fmt", "--all"]);
+ break;
+ default:
+ throw new Error(
+ "Usage: vp run engine:build|engine:test|engine:lint|engine:fmt|engine:fmt-fix",
+ );
+}
diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json
new file mode 100644
index 000000000..16537f27f
--- /dev/null
+++ b/scripts/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../packages/typescript-config/base.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "target": "ES2022"
+ },
+ "include": ["*.ts"]
+}
From 0ddcb84954abcbfe229eeea45c3db34e6fc71903 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:01:21 +0530
Subject: [PATCH 06/93] feat(engine): prove browser Wasm through Start
Cloudflare assets and CSP
---
frontend/__tests__/start/document.spec.ts | 4 +-
frontend/e2e/engine-proof.migration.ts | 93 +++++++++++
frontend/package.json | 1 +
frontend/src/app/env.d.ts | 1 +
frontend/src/routeTree.gen.ts | 21 +++
frontend/src/routes/engine-proof.tsx | 148 ++++++++++++++++++
frontend/src/server/response-policy.server.ts | 2 +-
frontend/vite-plugins/typing-engine.ts | 15 ++
frontend/vite.config.ts | 5 +
packages/typing-engine/package.json | 13 ++
packages/typing-engine/src/browser.client.ts | 117 ++++++++++++++
packages/typing-engine/tsconfig.json | 11 ++
pnpm-lock.yaml | 14 ++
13 files changed, 443 insertions(+), 2 deletions(-)
create mode 100644 frontend/e2e/engine-proof.migration.ts
create mode 100644 frontend/src/routes/engine-proof.tsx
create mode 100644 frontend/vite-plugins/typing-engine.ts
create mode 100644 packages/typing-engine/package.json
create mode 100644 packages/typing-engine/src/browser.client.ts
create mode 100644 packages/typing-engine/tsconfig.json
diff --git a/frontend/__tests__/start/document.spec.ts b/frontend/__tests__/start/document.spec.ts
index 8f08f15b6..187a8621c 100644
--- a/frontend/__tests__/start/document.spec.ts
+++ b/frontend/__tests__/start/document.spec.ts
@@ -44,12 +44,14 @@ describe("Start document and response policy in workerd", () => {
expect(directives.get("frame-src")).toEqual([
"https://challenges.cloudflare.com",
]);
- expect(directives.get("script-src")).toHaveLength(3);
+ expect(directives.get("script-src")).toHaveLength(4);
expect(directives.get("script-src")).toEqual([
"'self'",
+ "'wasm-unsafe-eval'",
"https://challenges.cloudflare.com",
expect.stringMatching(/^'nonce-[^']+'$/),
]);
+ expect(directives.get("script-src")).not.toContain("'unsafe-eval'");
});
it("renders static fallbacks and gives every executable script a fresh CSP nonce", async () => {
diff --git a/frontend/e2e/engine-proof.migration.ts b/frontend/e2e/engine-proof.migration.ts
new file mode 100644
index 000000000..086561677
--- /dev/null
+++ b/frontend/e2e/engine-proof.migration.ts
@@ -0,0 +1,93 @@
+import { readFile, writeFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { expect, test } from "playwright/test";
+
+const root = resolve(import.meta.dirname, "../../packages/typing-fixtures");
+test("native fixture matches browser Wasm through built Cloudflare assets", async ({
+ page,
+}) => {
+ const errors: string[] = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await page.goto("/engine-proof");
+ const response = page.waitForResponse((response) =>
+ response.url().endsWith(".wasm"),
+ );
+ await page
+ .getByRole("textbox", { name: "Command fixture" })
+ .fill(await readFile(resolve(root, "commands/finite-edit.json"), "utf8"));
+ await page.getByRole("button", { name: "Run fixture" }).click();
+ const wasm = await response;
+ expect(wasm.status()).toBe(200);
+ expect(wasm.headers()["content-type"]).toContain("application/wasm");
+ await expect(page.getByTestId("engine-proof-result")).not.toBeEmpty();
+ const actual: unknown = JSON.parse(
+ await page.getByTestId("engine-proof-result").innerText(),
+ );
+ const expected: unknown = JSON.parse(
+ await readFile(resolve(root, "oracle/finite-edit.json"), "utf8"),
+ );
+ expect(actual).toEqual(expected);
+ expect(errors).toEqual([]);
+ await page.getByRole("textbox", { name: "Type finite slice" }).fill("");
+ const inputToPaint: number[] = [];
+ for (const character of "one two") {
+ await page.evaluate(() => {
+ const input = document.querySelector(
+ 'input[aria-label="Type finite slice"]',
+ );
+ if (input === null) throw new Error("Missing slice input");
+ input.addEventListener(
+ "beforeinput",
+ () => performance.mark("slice-input"),
+ { once: true },
+ );
+ });
+ await page
+ .getByRole("textbox", { name: "Type finite slice" })
+ .pressSequentially(character);
+ inputToPaint.push(
+ await page.evaluate(async () => {
+ await new Promise((resolve) =>
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
+ );
+ return (
+ performance.now() -
+ (performance.getEntriesByName("slice-input").at(-1)?.startTime ??
+ performance.now())
+ );
+ }),
+ );
+ }
+ await expect(page.getByLabel("Finite slice state")).toHaveText("finished");
+ const output = process.env["ENGINE_PROOF_OUTPUT"];
+ if (output !== undefined) {
+ const benchmark: unknown = JSON.parse(
+ await page.getByTestId("engine-proof-benchmark").innerText(),
+ );
+ await writeFile(
+ output,
+ `${JSON.stringify({ fixture: "built Cloudflare Start; Desktop Chrome; 50 warmed samples per boundary; 200 UTF-16 units per sample; 7 real keyboard commits with double-rAF paint", browser: page.context().browser()?.version(), wasmBytes: (await wasm.body()).byteLength, wasmHeaders: wasm.headers(), inputToPaint, benchmark }, null, 2)}\n`,
+ );
+ }
+ // Invalid creation must be recoverable; subsequent valid creation still works.
+ await page.getByRole("textbox", { name: "Command fixture" }).fill(
+ JSON.stringify({
+ name: "invalid",
+ config: { mode: "invalid" },
+ words: [],
+ commands: [],
+ }),
+ );
+ await page.getByRole("button", { name: "Run fixture" }).click();
+ await expect(page.getByTestId("engine-proof-error")).toContainText(
+ "Invalid mode",
+ );
+ await page
+ .getByRole("textbox", { name: "Command fixture" })
+ .fill(await readFile(resolve(root, "commands/finite-edit.json"), "utf8"));
+ await page.getByRole("button", { name: "Run fixture" }).click();
+ await expect(page.getByTestId("engine-proof-result")).not.toBeEmpty();
+ expect(
+ JSON.parse(await page.getByTestId("engine-proof-result").innerText()),
+ ).toEqual(expected);
+});
diff --git a/frontend/package.json b/frontend/package.json
index 9b25cbb7e..2257c7703 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -60,6 +60,7 @@
"@oxytype/funbox": "workspace:*",
"@oxytype/schemas": "workspace:*",
"@oxytype/typing-core": "workspace:*",
+ "@oxytype/typing-engine": "workspace:*",
"@oxytype/util": "workspace:*",
"@sentry/browser": "11.1.0",
"@sentry/vite-plugin": "5.4.0",
diff --git a/frontend/src/app/env.d.ts b/frontend/src/app/env.d.ts
index 5a54c8e7c..8ffe023c7 100644
--- a/frontend/src/app/env.d.ts
+++ b/frontend/src/app/env.d.ts
@@ -1,5 +1,6 @@
// oxlint-disable-next-line typescript/consistent-type-definitions -- Vite augments this interface.
interface ImportMetaEnv {
+ readonly OXYTYPE_ENGINE_PROOF: boolean;
readonly OXYTYPE_OFFLINE: boolean;
readonly OXYTYPE_BUILD_ID: string;
readonly OXYTYPE_SENTRY_ORIGIN: string;
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index cc84bcc03..48cb6b5c4 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as R404DothtmlRouteImport } from './routes/404[.]html'
import { Route as AppRouteImport } from './routes/_app'
+import { Route as EngineProofRouteImport } from './routes/engine-proof'
import { Route as OauthCallbackDothtmlRouteImport } from './routes/oauth-callback[.]html'
import { Route as PrivacyPolicyDothtmlRouteImport } from './routes/privacy-policy[.]html'
import { Route as AppIndexRouteImport } from './routes/_app.index'
@@ -99,6 +100,11 @@ const AppRoute = AppRouteImport.update({
id: '/_app',
getParentRoute: () => rootRouteImport,
} as any)
+const EngineProofRoute = EngineProofRouteImport.update({
+ id: '/engine-proof',
+ path: '/engine-proof',
+ getParentRoute: () => rootRouteImport,
+} as any)
const OauthCallbackDothtmlRoute = OauthCallbackDothtmlRouteImport.update({
id: '/oauth-callback.html',
path: '/oauth-callback.html',
@@ -506,6 +512,7 @@ const ApiUsersTagsTagIdPersonalBestRoute =
export interface FileRoutesByFullPath {
'/404.html': typeof R404DothtmlRoute
'/': typeof AppIndexRoute
+ '/engine-proof': typeof EngineProofRoute
'/oauth-callback.html': typeof OauthCallbackDothtmlRoute
'/privacy-policy.html': typeof PrivacyPolicyDothtmlRoute
'/leaderboards': typeof AppLeaderboardsRoute
@@ -585,6 +592,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/404.html': typeof R404DothtmlRoute
+ '/engine-proof': typeof EngineProofRoute
'/oauth-callback.html': typeof OauthCallbackDothtmlRoute
'/privacy-policy.html': typeof PrivacyPolicyDothtmlRoute
'/': typeof AppIndexRoute
@@ -667,6 +675,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/404.html': typeof R404DothtmlRoute
'/_app': typeof AppRouteWithChildren
+ '/engine-proof': typeof EngineProofRoute
'/oauth-callback.html': typeof OauthCallbackDothtmlRoute
'/privacy-policy.html': typeof PrivacyPolicyDothtmlRoute
'/_app/_authenticated': typeof AppAuthenticatedRouteWithChildren
@@ -751,6 +760,7 @@ export interface FileRouteTypes {
fullPaths:
| '/404.html'
| '/'
+ | '/engine-proof'
| '/oauth-callback.html'
| '/privacy-policy.html'
| '/leaderboards'
@@ -830,6 +840,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/404.html'
+ | '/engine-proof'
| '/oauth-callback.html'
| '/privacy-policy.html'
| '/'
@@ -911,6 +922,7 @@ export interface FileRouteTypes {
| '__root__'
| '/404.html'
| '/_app'
+ | '/engine-proof'
| '/oauth-callback.html'
| '/privacy-policy.html'
| '/_app/_authenticated'
@@ -994,6 +1006,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
R404DothtmlRoute: typeof R404DothtmlRoute
AppRoute: typeof AppRouteWithChildren
+ EngineProofRoute: typeof EngineProofRoute
OauthCallbackDothtmlRoute: typeof OauthCallbackDothtmlRoute
PrivacyPolicyDothtmlRoute: typeof PrivacyPolicyDothtmlRoute
ApiSplatRoute: typeof ApiSplatRoute
@@ -1032,6 +1045,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AppRouteImport
parentRoute: typeof rootRouteImport
}
+ '/engine-proof': {
+ id: '/engine-proof'
+ path: '/engine-proof'
+ fullPath: '/engine-proof'
+ preLoaderRoute: typeof EngineProofRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/oauth-callback.html': {
id: '/oauth-callback.html'
path: '/oauth-callback.html'
@@ -1869,6 +1889,7 @@ const ApiPrefixDocsRouteWithChildren = ApiPrefixDocsRoute._addFileChildren(
const rootRouteChildren: RootRouteChildren = {
R404DothtmlRoute: R404DothtmlRoute,
AppRoute: AppRouteWithChildren,
+ EngineProofRoute: EngineProofRoute,
OauthCallbackDothtmlRoute: OauthCallbackDothtmlRoute,
PrivacyPolicyDothtmlRoute: PrivacyPolicyDothtmlRoute,
ApiSplatRoute: ApiSplatRoute,
diff --git a/frontend/src/routes/engine-proof.tsx b/frontend/src/routes/engine-proof.tsx
new file mode 100644
index 000000000..cf3f6d29a
--- /dev/null
+++ b/frontend/src/routes/engine-proof.tsx
@@ -0,0 +1,148 @@
+import type {
+ BrowserSession,
+ Command,
+ Config,
+ Text,
+} from "@oxytype/typing-engine/browser.client";
+
+import { createFileRoute } from "@tanstack/solid-router";
+import { createSignal, onCleanup, Show } from "solid-js";
+
+export const Route = createFileRoute("/engine-proof")({
+ component: EngineProof,
+});
+
+// Build-time opt-in test harness. Regular deployment exposes no runnable harness.
+function EngineProof() {
+ const [fixture, setFixture] = createSignal("");
+ const [result, setResult] = createSignal("");
+ const [failure, setFailure] = createSignal("");
+ const [benchmark, setBenchmark] = createSignal("");
+ const [sliceInput, setSliceInput] = createSignal("");
+ const [sliceEnded, setSliceEnded] = createSignal(false);
+ let typingSession: BrowserSession | undefined;
+ let disposed = false;
+ const handles = new Set();
+ onCleanup(() => {
+ disposed = true;
+ for (const handle of handles) handle.dispose();
+ handles.clear();
+ });
+ const run = async (): Promise => {
+ setResult("");
+ setFailure("");
+ try {
+ const input = JSON.parse(fixture()) as {
+ name: string;
+ config: Partial;
+ words: Text[];
+ commands: Command[];
+ };
+ const started = performance.now();
+ const { loadEngine, benchmarkBoundary, encodeText } =
+ await import("@oxytype/typing-engine/browser.client");
+ const engine = await loadEngine();
+ if (disposed) return;
+ const initializationMs = performance.now() - started;
+ typingSession?.dispose();
+ typingSession = engine.createSession({ mode: "words", words: 2 }, [
+ encodeText("one "),
+ encodeText("two"),
+ ]);
+ handles.add(typingSession);
+ setSliceInput("");
+ setSliceEnded(false);
+ const session = engine.createSession(input.config, input.words);
+ const isolated = engine.createSession(input.config, input.words);
+ handles.add(session);
+ handles.add(isolated);
+ const untouched = JSON.stringify(isolated.log());
+ const frames = input.commands.map((command) => ({
+ state: session.command(command).state,
+ log: session.log(),
+ }));
+ if (untouched !== JSON.stringify(isolated.log())) {
+ throw new Error("Session isolation failed");
+ }
+ session.dispose();
+ session.dispose();
+ let staleRejected = false;
+ try {
+ session.log();
+ } catch {
+ staleRejected = true;
+ }
+ if (!staleRejected) throw new Error("Stale session accepted");
+ isolated.dispose();
+ handles.delete(session);
+ handles.delete(isolated);
+ const recreated = engine.createSession(input.config, input.words);
+ recreated.dispose();
+ setBenchmark(
+ JSON.stringify({
+ initializationMs,
+ ...benchmarkBoundary(),
+ memoryBytes: engine.memoryBytes(),
+ }),
+ );
+ setResult(JSON.stringify({ name: input.name, frames }));
+ } catch (error) {
+ for (const handle of handles) handle.dispose();
+ handles.clear();
+ setFailure(error instanceof Error ? error.message : String(error));
+ }
+ };
+ return (
+ Unavailable
}
+ >
+
+
+
+ {result()}
+ {failure()}
+ {benchmark()}
+ {
+ event.preventDefault();
+ if (typingSession === undefined) return;
+ const observation = event as InputEvent;
+ if (observation.data === null) return;
+ const data = Array.from(
+ { length: observation.data.length },
+ (_, index) => observation.data?.charCodeAt(index) ?? 0,
+ );
+ const update = typingSession.command({
+ op: "insert",
+ data,
+ now: performance.now(),
+ date: Date.now(),
+ automatic: null,
+ correctShiftUsed: null,
+ isCompositionEnding: null,
+ });
+ setSliceInput(String.fromCharCode(...update.state.inputValue));
+ setSliceEnded(update.state.ended);
+ }}
+ />
+
+
+
+ );
+}
diff --git a/frontend/src/server/response-policy.server.ts b/frontend/src/server/response-policy.server.ts
index 553f358c7..6233470d5 100644
--- a/frontend/src/server/response-policy.server.ts
+++ b/frontend/src/server/response-policy.server.ts
@@ -36,7 +36,7 @@ export function applyResponsePolicy(
"frame-ancestors 'self'",
"object-src 'none'",
"frame-src https://challenges.cloudflare.com",
- `script-src 'self' https://challenges.cloudflare.com 'nonce-${nonce}'${development ? " 'unsafe-eval'" : ""}`,
+ `script-src 'self' 'wasm-unsafe-eval' https://challenges.cloudflare.com 'nonce-${nonce}'${development ? " 'unsafe-eval'" : ""}`,
`script-src-attr 'unsafe-hashes' 'sha256-${recoveryHash}'`,
"style-src 'self' 'unsafe-inline'",
"font-src 'self' data:",
diff --git a/frontend/vite-plugins/typing-engine.ts b/frontend/vite-plugins/typing-engine.ts
new file mode 100644
index 000000000..2fef89a25
--- /dev/null
+++ b/frontend/vite-plugins/typing-engine.ts
@@ -0,0 +1,15 @@
+import { execFileSync } from "node:child_process";
+import { resolve } from "node:path";
+
+/** Every Vite entry (dev, Start, staging, production) bootstraps ignored bindings. */
+export function prepareTypingEngine(): void {
+ const root = resolve(import.meta.dirname, "../..");
+ execFileSync(
+ process.execPath,
+ [resolve(root, "scripts/engine.ts"), "build"],
+ {
+ cwd: root,
+ stdio: "inherit",
+ },
+ );
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 90b9b449b..76d900d25 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -7,6 +7,7 @@ import { tanstackStart } from "@tanstack/solid-start/plugin/vite";
import solid from "vite-plugin-solid";
import tailwindcss from "@tailwindcss/vite";
import { frontendLint } from "./lint.config";
+import { prepareTypingEngine } from "./vite-plugins/typing-engine";
import { fontStyles } from "./vite-plugins/font-styles";
import { workerNodeBuiltins } from "./vite-plugins/worker-node-builtins";
import { apiDocs } from "./vite-plugins/api-docs";
@@ -40,6 +41,7 @@ async function devPlugins(env: Record): Promise {
}
export default defineConfig(async ({ mode, command }) => {
+ prepareTypingEngine();
const env = loadEnv(mode, process.cwd(), "");
const buildId = resolveBuildId(mode !== "production");
if (mode === "production") {
@@ -167,6 +169,9 @@ export default defineConfig(async ({ mode, command }) => {
...(command === "serve" ? await devPlugins(env) : []),
],
define: {
+ "import.meta.env.OXYTYPE_ENGINE_PROOF": JSON.stringify(
+ process.env["OXYTYPE_ENGINE_PROOF"] === "true",
+ ),
"import.meta.env.OXYTYPE_OFFLINE": JSON.stringify(mode === "offline"),
// Client and Worker share it; logs and error reports join on this ID.
"import.meta.env.OXYTYPE_BUILD_ID": JSON.stringify(buildId),
diff --git a/packages/typing-engine/package.json b/packages/typing-engine/package.json
new file mode 100644
index 000000000..8a7ef8160
--- /dev/null
+++ b/packages/typing-engine/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@oxytype/typing-engine",
+ "private": true,
+ "license": "GPL-3.0",
+ "type": "module",
+ "exports": {
+ "./browser.client": "./src/browser.client.ts"
+ },
+ "devDependencies": {
+ "@oxytype/typescript-config": "workspace:*",
+ "vite-plus": "catalog:"
+ }
+}
diff --git a/packages/typing-engine/src/browser.client.ts b/packages/typing-engine/src/browser.client.ts
new file mode 100644
index 000000000..ac8303674
--- /dev/null
+++ b/packages/typing-engine/src/browser.client.ts
@@ -0,0 +1,117 @@
+import initialize, {
+ binding_version,
+ WasmSession,
+} from "../generated/engine.js";
+import wasmUrl from "../generated/engine_bg.wasm?url";
+import type { Command } from "../generated/types/Command";
+import type { Config } from "../generated/types/Config";
+import type { Text } from "../generated/types/Text";
+import type { Update } from "../generated/types/Update";
+
+export type { Command, Config, Text, Update };
+const BINDING_VERSION = 1;
+
+export function encodeText(value: string): Text {
+ return Array.from({ length: value.length }, (_, index) =>
+ value.charCodeAt(index),
+ );
+}
+export function decodeText(units: Text): string {
+ let value = "";
+ for (let i = 0; i < units.length; i += 4096) {
+ value += String.fromCharCode(...units.slice(i, i + 4096));
+ }
+ return value;
+}
+
+/** Module preparation is async; all prepared session commands are synchronous. */
+export async function loadEngine(): Promise<{
+ createSession: (config: Partial, words: Text[]) => BrowserSession;
+ memoryBytes: () => number;
+}> {
+ const module = await initialize({ module_or_path: wasmUrl });
+ if (binding_version() !== BINDING_VERSION) {
+ throw new Error("Typing engine binding version mismatch");
+ }
+ return {
+ createSession: (config, words) => new BrowserSession(config, words),
+ memoryBytes: () => module.memory.buffer.byteLength,
+ };
+}
+
+export class BrowserSession {
+ private handle: WasmSession | undefined;
+
+ constructor(config: Partial, words: Text[]) {
+ this.handle = new WasmSession(
+ JSON.stringify(config),
+ JSON.stringify(words),
+ BINDING_VERSION,
+ );
+ }
+ command(command: Command): Update {
+ if (this.handle === undefined) {
+ throw new Error("Typing engine session disposed");
+ }
+ return JSON.parse(this.handle.command(JSON.stringify(command))) as Update;
+ }
+ log(): unknown {
+ if (this.handle === undefined) {
+ throw new Error("Typing engine session disposed");
+ }
+ return JSON.parse(this.handle.log()) as unknown;
+ }
+ commandObject(command: Command): Update {
+ if (this.handle === undefined) {
+ throw new Error("Typing engine session disposed");
+ }
+ return this.handle.command_value(command) as Update;
+ }
+ dispose(): void {
+ const handle = this.handle;
+ if (handle === undefined) return;
+ this.handle = undefined;
+ handle.dispose();
+ handle.free();
+ }
+}
+
+/** Compare incremental object conversion with a JSON boundary, same core. */
+export function benchmarkBoundary(): { jsonMs: number[]; objectMs: number[] } {
+ const jsonMs: number[] = [];
+ const objectMs: number[] = [];
+ const config: Partial = { mode: "words", words: 0 };
+ const words = Array.from({ length: 101 }, () => encodeText("a "));
+ for (let sample = 0; sample < 60; sample++) {
+ for (const [object, measurements] of [
+ [false, jsonMs],
+ [true, objectMs],
+ ] as const) {
+ const session = new BrowserSession(config, words);
+ try {
+ const start = performance.now();
+ for (let index = 0; index < 100; index++) {
+ const command: Command = {
+ op: "insert",
+ data: [97, 32],
+ now: index * 100,
+ date: 0,
+ automatic: null,
+ isCompositionEnding: null,
+ correctShiftUsed: null,
+ };
+ const update = object
+ ? session.commandObject(command)
+ : session.command(command);
+ if (update.state.wordIndex !== index + 1) {
+ throw new Error("Boundary benchmark lost progress");
+ }
+ }
+ if (sample >= 10) measurements.push((performance.now() - start) / 200);
+ } finally {
+ session.dispose();
+ }
+ }
+ }
+ return { jsonMs, objectMs };
+}
diff --git a/packages/typing-engine/tsconfig.json b/packages/typing-engine/tsconfig.json
new file mode 100644
index 000000000..5b2c1ab1b
--- /dev/null
+++ b/packages/typing-engine/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@oxytype/typescript-config/base.json",
+ "compilerOptions": {
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "target": "ES2022",
+ "lib": ["es2022", "dom"],
+ "types": ["vite-plus/client"]
+ },
+ "include": ["src", "generated"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index aa60498c1..351686784 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -415,6 +415,9 @@ importers:
'@oxytype/typing-core':
specifier: workspace:*
version: link:../packages/typing-core
+ '@oxytype/typing-engine':
+ specifier: workspace:*
+ version: link:../packages/typing-engine
'@oxytype/util':
specifier: workspace:*
version: link:../packages/util
@@ -886,6 +889,17 @@ importers:
specifier: 'catalog:'
version: 1.0.0(@opentelemetry/api@1.9.1)(@types/node@24.19.0)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(esbuild@0.28.1)(happy-dom@20.14.5)(jiti@2.7.0)(jsdom@30.1.1(@noble/hashes@1.8.0))(sass@1.105.1)(terser@5.49.1)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1)
+ packages/typing-engine:
+ devDependencies:
+ '@oxytype/typescript-config':
+ specifier: workspace:*
+ version: link:../typescript-config
+ vite-plus:
+ specifier: 'catalog:'
+ version: 1.0.0(@opentelemetry/api@1.9.1)(@types/node@24.19.0)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(esbuild@0.28.1)(happy-dom@20.14.5)(jiti@2.7.0)(jsdom@30.1.1(@noble/hashes@1.8.0))(sass@1.105.1)(terser@5.49.1)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1)
+
+ packages/typing-fixtures: {}
+
packages/util:
devDependencies:
'@oxytype/typescript-config':
From b5bc5267237f9e11c292d2beed463866275c4391 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:01:42 +0530
Subject: [PATCH 07/93] ci(engine): bootstrap pinned Rust and test native
browser Wasm paths
---
.github/actions/setup-engine/action.yml | 16 +++++++++++
.github/actions/setup/action.yml | 2 ++
.github/workflows/oxytype-ci.yml | 35 +++++++++++++++++++++++-
.github/workflows/production-release.yml | 3 ++
4 files changed, 55 insertions(+), 1 deletion(-)
create mode 100644 .github/actions/setup-engine/action.yml
diff --git a/.github/actions/setup-engine/action.yml b/.github/actions/setup-engine/action.yml
new file mode 100644
index 000000000..def9a7e43
--- /dev/null
+++ b/.github/actions/setup-engine/action.yml
@@ -0,0 +1,16 @@
+name: Set up Rust typing engine
+description: Bootstrap pinned Rust, Wasm target/tooling and ignored browser bindings.
+runs:
+ using: composite
+ steps:
+ - name: Restore Rust build cache
+ uses: actions/cache@v5.0.3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/bin/wasm-bindgen*
+ target
+ key: engine-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
+ - name: Prepare pinned Rust and Wasm bindings
+ shell: bash
+ run: vp run engine:build
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 59f865de2..fe2d7597b 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -15,3 +15,5 @@ runs:
node-version-file: .node-version
cache: true
cache-save: ${{ inputs.save-cache }}
+ - name: Prepare generated engine bindings
+ uses: ./.github/actions/setup-engine
diff --git a/.github/workflows/oxytype-ci.yml b/.github/workflows/oxytype-ci.yml
index 175d90915..f4db0d57d 100644
--- a/.github/workflows/oxytype-ci.yml
+++ b/.github/workflows/oxytype-ci.yml
@@ -41,6 +41,7 @@ jobs:
packages: ${{ steps.select.outputs.packages }}
assets: ${{ steps.select.outputs.assets }}
code: ${{ steps.select.outputs.code }}
+ engine: ${{ steps.select.outputs.engine }}
steps:
# PRs read changed files from the API, so no checkout is needed.
@@ -72,12 +73,22 @@ jobs:
- 'packages/oxlint-config/**'
# Workspace packages bundled into the backend and frontend.
runtime-packages:
- - 'packages/{challenges,contracts,funbox,schemas,typing-core,util}/**'
+ - 'packages/{challenges,contracts,funbox,schemas,typing-core,typing-engine,typing-fixtures,util}/**'
+ - 'crates/**'
+ - 'Cargo.{toml,lock}'
+ - 'rust-toolchain.toml'
+ - 'scripts/engine.ts'
- '!packages/*/{__test__,__tests__,__fixtures__}/**'
- '!**/*.md'
packages:
- 'packages/**'
- '!**/*.md'
+ engine:
+ - 'crates/**'
+ - 'Cargo.{toml,lock}'
+ - 'rust-toolchain.toml'
+ - 'scripts/engine.ts'
+ - 'packages/typing-{engine,fixtures}/**'
backend:
- 'backend/**'
- '!backend/__tests__/**'
@@ -176,6 +187,27 @@ jobs:
enable packages packages lint-config static-layouts static-challenges
enable assets assets runtime-packages
enable code code
+ enable engine engine runtime-packages browser-tests
+
+ engine:
+ name: engine
+ needs: [changes]
+ if: needs.changes.outputs.engine == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v7.0.1
+ - uses: ./.github/actions/setup
+ - name: Native formatting, lint and parity
+ run: vp run engine:fmt && vp run engine:lint && vp run engine:test
+ - name: Build Cloudflare browser proof
+ env:
+ OXYTYPE_ENGINE_PROOF: "true"
+ run: vp run --filter @oxytype/frontend build:start
+ - name: Install Chromium
+ run: vp -C frontend exec playwright install --with-deps --only-shell chromium
+ - name: Browser Wasm parity
+ run: vp run --filter @oxytype/frontend benchmark-migration engine-proof.migration.ts
quality:
name: quality
@@ -475,6 +507,7 @@ jobs:
start-delivery,
ci-assets,
ci-pkg,
+ engine,
]
runs-on: ubuntu-latest
timeout-minutes: 2
diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml
index eb9c5916a..96ac04151 100644
--- a/.github/workflows/production-release.yml
+++ b/.github/workflows/production-release.yml
@@ -85,6 +85,9 @@ jobs:
- name: Install dependencies
run: vp install --frozen-lockfile
+ - name: Prepare pinned Rust and browser Wasm
+ uses: ./.github/actions/setup-engine
+
- name: Build production frontend and Worker
env:
OXYTYPE_BUILD_ID: ${{ env.DEPLOYMENT_SHA }}-${{ github.run_id }}-${{ github.run_attempt }}
From 9013d4dab518a218aa43f6b031cf4a12fac3a09f Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:02:41 +0530
Subject: [PATCH 08/93] docs(engine): record Cloudflare proof and freeze
performance budgets
---
docs/rust-engine/performance-budgets.json | 18 ++++++++
docs/rust-engine/performance-budgets.md | 53 +++++++++++++++++++++++
docs/rust-engine/progress.md | 13 ++++--
docs/rust-engine/proof-performance.json | 51 ++++++++++++++++++++++
4 files changed, 132 insertions(+), 3 deletions(-)
create mode 100644 docs/rust-engine/performance-budgets.json
create mode 100644 docs/rust-engine/performance-budgets.md
create mode 100644 docs/rust-engine/proof-performance.json
diff --git a/docs/rust-engine/performance-budgets.json b/docs/rust-engine/performance-budgets.json
new file mode 100644
index 000000000..2166010a6
--- /dev/null
+++ b/docs/rust-engine/performance-budgets.json
@@ -0,0 +1,18 @@
+{
+ "sourceCommit": "b5bc52672",
+ "wasmRawBytes": 1048576,
+ "wasmGzipBytes": 307200,
+ "glueRawBytes": 32768,
+ "bindingP95Ms": 0.1,
+ "initializationP95Ms": 100,
+ "inputToPaintP95Ms": 112.5,
+ "restartMedianMs": 448.49853500000063,
+ "restartP95Ms": 501.58758399999897,
+ "coldMedianMs": 820.9000000059605,
+ "coldP95Ms": 1500.6000000089407,
+ "warmMedianMs": 719.7000000029802,
+ "warmP95Ms": 844.4000000059605,
+ "activeHeapBytes": 22084844,
+ "resetHeapBytes": 15232636,
+ "wasmHeapBytesAfterReset": 16777216
+}
diff --git a/docs/rust-engine/performance-budgets.md b/docs/rust-engine/performance-budgets.md
new file mode 100644
index 000000000..9934de3e8
--- /dev/null
+++ b/docs/rust-engine/performance-budgets.md
@@ -0,0 +1,53 @@
+# Performance budgets — frozen before full port
+
+Budgets: `performance-budgets.json`. Do not raise them to hide a failed migration.
+Conditions: baseline.md; same Linux ARM64 machine, Desktop Chromium 151.0.7896.0,
+no CPU/network throttle, local built Cloudflare Start/workerd. Shared host; allow
+only the fixed additive margins below. CI timing is diagnostic until equivalently
+controlled; structural/size/parity assertions remain mandatory everywhere.
+
+## Early proof measured
+
+- Actual browser through production Vite/Start/Cloudflare build: pass.
+- Real keyboard finite test, deletion fixture, two isolated sessions, double disposal,
+ stale access rejection, recreation, invalid-config recovery: pass.
+- Wasm 484,654 raw bytes; gzip 110,209 bytes. MIME application/wasm; immutable asset headers.
+- Module/glue fetch + initialization: 22.1 ms (one cold sample; not a p95 claim).
+- JSON incremental boundary: median 0.005 ms, p95 0.006 ms per UTF-16 unit.
+- Incremental object boundary: median 0.008 ms, p95 0.0095 ms. 50 measured batches,
+ 10 discarded warmup batches, 200 UTF-16 units/batch, identical shared core.
+- Seven real input-to-double-rAF samples: maximum 36.1 ms. Full website/input soak
+ regression is still Stage 2 work; this proof uses a minimal owned Solid component.
+- Server JS bundle scan found no engine_bg, wasm_bindgen, WebAssembly, browser.client.
+- CSP requires wasm-unsafe-eval. General JavaScript eval remains denied in production;
+ nonce/static fallbacks unchanged. Workerd document policy: six tests pass.
+
+## Chosen boundary and limits
+
+Use synchronous JSON commands + bounded deltas after async preparation; retain
+object path as measurement control until Phase 1.7. JSON won this benchmark.
+No full history is returned per key; explicit log reads only.
+Raw Wasm <=1 MiB, gzip <=300 KiB, glue <=32 KiB. Binding p95 <=0.1 ms/unit.
+Initialization p95 <=100 ms (collect repeated cold samples at final gate).
+Startup cold/warm median <=baseline+75 ms, p95 <=baseline+100 ms.
+Input paint p95 <=baseline+5 ms. Restart median <=baseline+25 ms, p95 <=baseline+50 ms.
+2,000-character active JS heap <=baseline+8 MiB; reset JS heap <=baseline+2 MiB;
+retained Wasm linear memory after reset <=16 MiB. Keep JS and Wasm memory separate.
+Longer soaks must demonstrate bounded growth per recorded event and no per-restart leak.
+
+## Verification commands
+
+```sh
+vp run engine:test
+vp run engine:fmt
+vp run engine:lint
+OXYTYPE_ENGINE_PROOF=true vp run --filter @oxytype/frontend build:start
+ENGINE_PROOF_OUTPUT="$PWD/docs/rust-engine/candidate-proof.json" vp run --filter @oxytype/frontend benchmark-migration engine-proof.migration.ts
+vp -C frontend test run --config vitest.start.config.ts __tests__/start/document.spec.ts
+```
+
+Generated declarations/Wasm/glue ignored; Vite bootstrap invokes pinned Cargo and
+wasm-bindgen automatically. CI and production rebuilds use the same setup action.
+Pinned candidate versions proven here: Rust 1.99.1, wasm-bindgen/CLI 0.2.129.
+Rust MSRV declared 1.99; only pinned 1.99.1 tested so far.
+
diff --git a/docs/rust-engine/progress.md b/docs/rust-engine/progress.md
index 058151e82..e712bc775 100644
--- a/docs/rust-engine/progress.md
+++ b/docs/rust-engine/progress.md
@@ -12,7 +12,7 @@ empty commits to inflate count. Do not publish production or releases implicitly
## Current position
-Stage 1, Phases 1.1–1.2 complete; Phase 1.3 in progress. Read root AGENTS.md and the complete migration
+Stage 1, Phases 1.1–1.3 complete; Phase 1.4 in progress. Read root AGENTS.md and the complete migration
plan before edits. Installed workspace dependencies with frozen lockfile.
Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen oracle and mismatch detection. Existing website engine remains authoritative.
@@ -20,8 +20,8 @@ Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen o
| --- | --- | --- |
| 1.1 Inventory / baselines | Complete | See inventory.md, features.md, baseline.md and raw measurements. |
| 1.2 Contracts / fixtures | Complete | 13 command traces, 15 oracle/integrity tests; contract and classified gaps. |
-| 1.3 Native / browser proof | Active | Implement finite slice; native/browser/Cloudflare parity and measured budgets pending. |
-| 1.4 Input / lifecycle / timing | Pending | Requires production browser proof. |
+| 1.3 Native / browser proof | Complete | Native 7 traces; Cloudflare real-browser finite slice; MIME/CSP, lifecycle, boundary benchmark. Budgets frozen. |
+| 1.4 Input / lifecycle / timing | Active | Remaining input penalties, code, physical observations, browser differences and timing. |
| 1.5 Generation / configuration | Pending | Requires contract and proof. |
| 1.6 Events / results / replay | Pending | Requires input and generation. |
| 1.7 Binding / engine gate | Pending | Full three-path parity and budgets. |
@@ -39,6 +39,13 @@ Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen o
- Phase 1.2 type-aware lint: pass. Capture created new traces once, refuses overwrite.
- `vp exec rustup target add wasm32-unknown-unknown`: pass.
+- Phase 1.3: `vp run engine:test`: three native tests pass (seven frozen traces).
+- Phase 1.3: `vp run engine:lint` and `vp run engine:fmt`: pass.
+- Phase 1.3: opted-in Cloudflare production build and browser proof: pass.
+- Phase 1.3: workerd document suite: six pass; production CSP now permits Wasm compilation specifically.
+- Phase 1.3: boundary benchmark/type-aware lint passed; proof-performance.json and performance-budgets.md record limits and evidence.
+- Early failed browser proof correctly exposed CSP; fixed and retested. Initial build raced workspace dependency installation; reran after install completed.
+
## Decisions
- User's three-stage PR structure overrides the plan's suggested per-phase PRs.
diff --git a/docs/rust-engine/proof-performance.json b/docs/rust-engine/proof-performance.json
new file mode 100644
index 000000000..075bd7507
--- /dev/null
+++ b/docs/rust-engine/proof-performance.json
@@ -0,0 +1,51 @@
+{
+ "fixture": "built Cloudflare Start; Desktop Chrome; 50 warmed samples per boundary; 200 UTF-16 units per sample; 7 real keyboard commits with double-rAF paint",
+ "browser": "153.0.8010.12",
+ "wasmBytes": 484654,
+ "wasmHeaders": {
+ "transfer-encoding": "chunked",
+ "cache-control": "public, max-age=31536000, immutable",
+ "content-encoding": "gzip",
+ "cf-cache-status": "HIT",
+ "etag": "\"8f627cfb164e1e2bdd63d31ce0abc8f8\"",
+ "content-type": "application/wasm",
+ "x-content-type-options": "nosniff"
+ },
+ "inputToPaint": [
+ 36.099999994039536, 32.3999999910593, 32.79999999701977, 22.69999998807907,
+ 29.299999997019768, 25.100000008940697, 24.400000005960464
+ ],
+ "benchmark": {
+ "initializationMs": 22.099999994039536,
+ "jsonMs": [
+ 0.005499999970197678, 0.006000000014901161, 0.005999999940395355,
+ 0.006000000014901161, 0.005, 0.005999999940395355, 0.005, 0.005,
+ 0.005499999970197678, 0.005999999940395355, 0.005499999970197678,
+ 0.005500000044703484, 0.005, 0.005499999970197678, 0.005, 0.005,
+ 0.0044999999552965166, 0.005499999970197678, 0.005, 0.0044999999552965166,
+ 0.005, 0.005, 0.005, 0.0044999999552965166, 0.005, 0.005, 0.005,
+ 0.004500000029802323, 0.003999999985098839, 0.005, 0.004500000029802323,
+ 0.005, 0.005, 0.003999999985098839, 0.005, 0.0044999999552965166,
+ 0.0044999999552965166, 0.0044999999552965166, 0.0044999999552965166,
+ 0.0044999999552965166, 0.0044999999552965166, 0.004500000029802323,
+ 0.004500000029802323, 0.004500000029802323, 0.005, 0.004000000059604645,
+ 0.005499999970197678, 0.004500000029802323, 0.005, 0.0044999999552965166
+ ],
+ "objectMs": [
+ 0.009499999955296516, 0.009499999955296516, 0.009000000059604645,
+ 0.009500000029802322, 0.0125, 0.00850000001490116, 0.009499999955296516,
+ 0.00899999998509884, 0.00850000001490116, 0.007999999970197678,
+ 0.00850000001490116, 0.008499999940395355, 0.00850000001490116,
+ 0.00850000001490116, 0.007999999970197678, 0.00850000001490116,
+ 0.007999999970197678, 0.00850000001490116, 0.008499999940395355,
+ 0.007999999970197678, 0.0075, 0.008000000044703484, 0.0075,
+ 0.007999999970197678, 0.007999999970197678, 0.0075, 0.0075, 0.0075,
+ 0.008000000044703484, 0.0075, 0.0075, 0.0075, 0.0075,
+ 0.008000000044703484, 0.0075, 0.006999999955296516, 0.0075, 0.0075,
+ 0.006999999955296516, 0.006999999955296516, 0.0075, 0.007999999970197678,
+ 0.007000000029802322, 0.0075, 0.0075, 0.0075, 0.0075, 0.0075, 0.0075,
+ 0.008000000044703484
+ ],
+ "memoryBytes": 1310720
+ }
+}
From d27c21c54a37a86f5a733b4c47a99477b1a9a50e Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:04:24 +0530
Subject: [PATCH 09/93] feat(engine): port error penalties difficulty and code
indentation rules
---
crates/typing-engine/src/session.rs | 153 ++++++++++++++++++++++--
crates/typing-engine/tests/slice.rs | 6 +
docs/rust-engine/performance-budgets.md | 2 +-
3 files changed, 147 insertions(+), 14 deletions(-)
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
index 7dc2ac9c6..ac84dd000 100644
--- a/crates/typing-engine/src/session.rs
+++ b/crates/typing-engine/src/session.rs
@@ -152,6 +152,7 @@ pub struct Session {
start: Option,
ticks: u32,
events: Vec,
+ indent_pending: bool,
}
impl Session {
@@ -173,6 +174,7 @@ impl Session {
start: None,
ticks: 0,
events: vec![],
+ indent_pending: false,
})
}
@@ -283,6 +285,7 @@ impl Session {
self.reason = None;
self.start = None;
self.ticks = 0;
+ self.indent_pending = false;
self.events.clear();
self.input = vec![Text::default(); self.words.len().max(1)];
}
@@ -312,6 +315,22 @@ impl Session {
shift: Option,
automatic: bool,
composition: bool,
+ ) {
+ self.insert_one(unit, now, date, shift, automatic, composition);
+ while self.indent_pending && !self.ended {
+ self.indent_pending = false;
+ self.insert_one(9, now, date, None, true, false);
+ }
+ }
+
+ fn insert_one(
+ &mut self,
+ unit: u16,
+ now: f64,
+ date: f64,
+ shift: Option,
+ automatic: bool,
+ composition: bool,
) {
let input = self.current();
let target = self.target();
@@ -330,6 +349,24 @@ impl Session {
target.0.get(input.len()).copied(),
&self.config.language,
);
+ let replacement = if unit == 0x2026 && target.0.get(input.len()) != Some(&unit) {
+ Some("...")
+ } else if unit == 0x0133
+ && self.config.language.starts_with("dutch")
+ && target.0.get(input.len()) != Some(&unit)
+ {
+ Some("ij")
+ } else {
+ None
+ };
+ if let Some(replacement) = replacement {
+ for unit in replacement.encode_utf16() {
+ if !self.ended {
+ self.insert(unit, now, date, shift, automatic, composition);
+ }
+ }
+ return;
+ }
let commit = is_space(unit) || unit == 10 || (nospace && input.len() + 1 == target.len());
if is_space(unit)
&& input.is_empty()
@@ -383,7 +420,19 @@ impl Session {
.resize(self.input.len().max(self.index + 1), Text::default());
self.input[self.index] = next;
self.record("input", now, data);
- // Full input/lifecycle port adds difficulty/deletion penalties after browser proof.
+ if !correct && !stopped && self.config.delete_on_error != "off" {
+ self.delete_error(now);
+ }
+ if self.config.mode != "zen"
+ && ((self.config.difficulty == "master" && !correct)
+ || (self.config.difficulty == "expert"
+ && commit
+ && !(input.is_empty() && (is_space(unit) || unit == 10))
+ && !word_correct))
+ {
+ self.end(now, date, Some("difficulty".into()));
+ return;
+ }
let finite = self.config.mode != "time"
&& self.config.mode != "zen"
&& !(self.config.mode == "words" && self.config.words == 0);
@@ -396,6 +445,67 @@ impl Session {
} else if advance && (self.index + 1 < self.words.len() || self.config.mode == "zen") {
self.index += 1;
}
+ if !self.ended
+ && correct
+ && self.config.language.starts_with("code")
+ && self.target().0.first() == Some(&9)
+ && self.target().0.get(self.current().len()) == Some(&9)
+ {
+ self.indent_pending = true;
+ }
+ }
+
+ fn log_delete(&mut self, kind: &str, now: f64, char_index: usize, next: Text, automatic: bool) {
+ let mut data = json!({"inputType":kind,"wordIndex":self.index,"charIndex":char_index,"inputValue":next});
+ if automatic {
+ data["automatic"] = json!(true);
+ }
+ self.record("input", now, data);
+ self.input
+ .resize(self.input.len().max(self.index + 1), Text::default());
+ self.input[self.index] = next;
+ }
+
+ fn previous_input(&self, kind: &str) -> Text {
+ let mut input = self.input.get(self.index).cloned().unwrap_or_default();
+ if kind == "deleteWordBackward" {
+ input.0.clear();
+ } else if self.nospace() || matches!(input.0.last(), Some(32 | 10)) {
+ input.0.pop();
+ }
+ input
+ }
+
+ fn delete_error(&mut self, now: f64) {
+ let input = self.current();
+ let whole = self.config.delete_on_error.starts_with("word");
+ let kind = if whole {
+ "deleteWordBackward"
+ } else {
+ "deleteContentBackward"
+ };
+ if !input.is_empty() {
+ let next = if whole {
+ Text::default()
+ } else {
+ Text(input.0[..input.len() - 1].to_vec())
+ };
+ self.log_delete(kind, now, input.len(), next, true);
+ if !whole && input.len() > 1 {
+ self.log_delete(
+ kind,
+ now,
+ input.len() - 1,
+ Text(input.0[..input.len() - 2].to_vec()),
+ true,
+ );
+ }
+ }
+ if self.config.delete_on_error.contains("hard") && input.len() <= 1 && self.index > 0 {
+ self.index -= 1;
+ let previous = self.previous_input(kind);
+ self.log_delete(kind, now, previous.len(), previous, true);
+ }
}
fn delete(&mut self, kind: &str, now: f64) {
@@ -419,15 +529,23 @@ impl Session {
{
return;
}
- let next = if input.is_empty() {
- self.index -= 1;
- let mut previous = prev;
- if kind == "deleteWordBackward" {
- previous.0.clear();
- } else if self.nospace() || matches!(previous.0.last(), Some(32 | 10)) {
- previous.0.pop();
- }
- previous
+ let unindent = self.config.language.starts_with("code")
+ && self.config.code_unindent_on_backspace
+ && !input.is_empty()
+ && input.0.iter().all(|c| *c == 9)
+ && self.target().0.starts_with(&input.0);
+ if unindent {
+ self.log_delete(
+ "deleteWordBackward",
+ now,
+ input.len(),
+ Text::default(),
+ false,
+ );
+ }
+ let next = if input.is_empty() || unindent {
+ self.index = self.index.saturating_sub(1);
+ self.previous_input(kind)
} else if kind == "deleteWordBackward" {
delete_word(&input)
} else {
@@ -435,13 +553,22 @@ impl Session {
next.0.pop();
next
};
- let char_index = if input.is_empty() {
+ let char_index = if input.is_empty() || unindent {
next.len()
} else {
input.len()
};
- self.record("input",now,json!({"inputType":kind,"wordIndex":self.index,"charIndex":char_index,"inputValue":next}));
- self.input[self.index] = next;
+ self.log_delete(
+ if unindent {
+ "deleteContentBackward"
+ } else {
+ kind
+ },
+ now,
+ char_index,
+ next,
+ false,
+ );
}
fn advance(&mut self, now: f64, date: f64) {
diff --git a/crates/typing-engine/tests/slice.rs b/crates/typing-engine/tests/slice.rs
index ac586c88f..a08a26cc3 100644
--- a/crates/typing-engine/tests/slice.rs
+++ b/crates/typing-engine/tests/slice.rs
@@ -37,6 +37,12 @@ fn finite_slice_matches_frozen_intermediate_oracle() {
"timer-stall",
"stop-letter",
"infinite-words",
+ "code-indent",
+ "master-finish-once",
+ "delete-error-letter",
+ "delete-error-letter_hard",
+ "delete-error-word",
+ "delete-error-word_hard",
] {
let fixture: Fixture = serde_json::from_slice(
&std::fs::read(root.join(format!("commands/{name}.json"))).unwrap(),
diff --git a/docs/rust-engine/performance-budgets.md b/docs/rust-engine/performance-budgets.md
index 9934de3e8..4750369b3 100644
--- a/docs/rust-engine/performance-budgets.md
+++ b/docs/rust-engine/performance-budgets.md
@@ -1,7 +1,7 @@
# Performance budgets — frozen before full port
Budgets: `performance-budgets.json`. Do not raise them to hide a failed migration.
-Conditions: baseline.md; same Linux ARM64 machine, Desktop Chromium 151.0.7896.0,
+Conditions: baseline.md; same Linux ARM64 machine, Desktop Chromium 153.0.8010.12,
no CPU/network throttle, local built Cloudflare Start/workerd. Shared host; allow
only the fixed additive margins below. CI timing is diagnostic until equivalently
controlled; structural/size/parity assertions remain mandatory everywhere.
From 780749787c725a9dbe9adf7636fb48532896cae9 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:06:39 +0530
Subject: [PATCH 10/93] feat(engine): record physical keys and preserve legacy
cleanup ordering
---
crates/typing-engine/src/session.rs | 187 ++-
crates/typing-engine/tests/recordings.rs | 129 ++
crates/typing-engine/tests/slice.rs | 1 +
.../scripts/capture-rust-oracle.ts | 8 +-
packages/typing-core/scripts/rust-oracle.ts | 7 +-
packages/typing-fixtures/commands.schema.json | 1 +
.../commands/physical-observations.json | 113 ++
.../oracle/physical-observations.json | 1139 +++++++++++++++++
8 files changed, 1577 insertions(+), 8 deletions(-)
create mode 100644 crates/typing-engine/tests/recordings.rs
create mode 100644 packages/typing-fixtures/commands/physical-observations.json
create mode 100644 packages/typing-fixtures/oracle/physical-observations.json
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
index ac84dd000..2a82eaf9d 100644
--- a/crates/typing-engine/src/session.rs
+++ b/crates/typing-engine/src/session.rs
@@ -4,6 +4,7 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
+use std::collections::BTreeMap;
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -105,6 +106,9 @@ pub enum Command {
event_type: String,
event_data: Value,
},
+ Finalize {
+ now: f64,
+ },
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
@@ -153,6 +157,8 @@ pub struct Session {
ticks: u32,
events: Vec,
indent_pending: bool,
+ pressed: Vec<(String, f64)>,
+ no_code_index: usize,
}
impl Session {
@@ -175,6 +181,8 @@ impl Session {
ticks: 0,
events: vec![],
indent_pending: false,
+ pressed: vec![],
+ no_code_index: 0,
})
}
@@ -237,7 +245,8 @@ impl Session {
| Command::End { now, .. }
| Command::Abandon { now, .. }
| Command::Reset { now }
- | Command::Observe { now, .. } => *now,
+ | Command::Observe { now, .. }
+ | Command::Finalize { now } => *now,
};
if !now.is_finite() || now.abs() > 9_007_199_254_740_991.0 {
return Err("Invalid timestamp".into());
@@ -288,10 +297,15 @@ impl Session {
self.indent_pending = false;
self.events.clear();
self.input = vec![Text::default(); self.words.len().max(1)];
+ self.pressed.clear();
+ self.no_code_index = 0;
}
- Command::Observe { .. } => {
- return Err("Observations not yet implemented in proof slice".into());
- }
+ Command::Observe {
+ now,
+ event_type,
+ event_data,
+ } => self.observe(&event_type, now, event_data)?,
+ Command::Finalize { .. } => self.finalize(),
}
let origin = self.origin();
let events = self
@@ -608,6 +622,171 @@ impl Session {
_ => 4,
}
}
+
+ fn observe(&mut self, kind: &str, now: f64, mut data: Value) -> Result<(), String> {
+ if !data.is_object() {
+ return Err("Observation data must be an object".into());
+ }
+ if kind == "keydown" || kind == "keyup" {
+ let code = data["code"].as_str().ok_or("Missing key code")?.to_owned();
+ let tracked = (code.len() == 4
+ && code.starts_with("Key")
+ && code.as_bytes()[3].is_ascii_uppercase())
+ || (code.len() == 6
+ && code.starts_with("Digit")
+ && code.as_bytes()[5].is_ascii_digit())
+ || [
+ "NumpadMultiply",
+ "NumpadSubtract",
+ "NumpadAdd",
+ "NumpadDecimal",
+ "NumpadEqual",
+ "NumpadDivide",
+ "Numpad0",
+ "Numpad1",
+ "Numpad2",
+ "Numpad3",
+ "Numpad4",
+ "Numpad5",
+ "Numpad6",
+ "Numpad7",
+ "Numpad8",
+ "Numpad9",
+ "Backquote",
+ "Minus",
+ "Equal",
+ "BracketLeft",
+ "BracketRight",
+ "Backslash",
+ "Semicolon",
+ "Quote",
+ "IntlBackslash",
+ "Comma",
+ "Period",
+ "Slash",
+ "Space",
+ "Enter",
+ "Tab",
+ "NoCode",
+ ]
+ .contains(&code.as_str());
+ let indexed_no_code = code.strip_prefix("NoCode").is_some_and(|suffix| {
+ !suffix.is_empty() && suffix.bytes().all(|c| c.is_ascii_digit())
+ });
+ if !tracked && !(kind == "keyup" && indexed_no_code) {
+ return Ok(());
+ }
+ let mut key = code.clone();
+ if kind == "keydown" {
+ if let Some(index) = self.pressed.iter().position(|(key, _)| key == &code) {
+ self.pressed.remove(index);
+ self.record("keyup", now, data.clone());
+ }
+ if code == "NoCode" {
+ key = format!("NoCode{}", self.no_code_index);
+ self.no_code_index += 1;
+ }
+ self.pressed.push((key.clone(), round2(now)));
+ } else {
+ if code == "NoCode" {
+ if self.no_code_index == 0 {
+ return Ok(());
+ }
+ key = format!("NoCode{}", self.no_code_index - 1);
+ if !self.pressed.iter().any(|(code, _)| code == &key) {
+ return Ok(());
+ }
+ self.no_code_index -= 1;
+ }
+ let Some(index) = self.pressed.iter().position(|(code, _)| code == &key) else {
+ return Ok(());
+ };
+ self.pressed.remove(index);
+ }
+ data["code"] = json!(key);
+ self.record(kind, now, data);
+ } else if kind == "composition" || kind == "timer" {
+ self.record(kind, now, data);
+ } else {
+ return Err("Unsupported observation type".into());
+ }
+ Ok(())
+ }
+
+ fn finalize(&mut self) {
+ let mut down = BTreeMap::new();
+ for event in &self.events {
+ if event.kind == "keydown" {
+ down.insert(
+ event.data["code"].as_str().unwrap_or_default().to_owned(),
+ event.now,
+ );
+ }
+ }
+ let mut durations = vec![];
+ for event in &self.events {
+ if event.kind != "keyup" {
+ continue;
+ }
+ if let Some(start) = down.remove(event.data["code"].as_str().unwrap_or_default()) {
+ let duration = event.now - start;
+ if duration > 0.0 {
+ durations.push(duration);
+ }
+ }
+ }
+ let average = if durations.is_empty() {
+ 80.0
+ } else {
+ round2(durations.iter().sum::() / durations.len() as f64)
+ };
+ for (key, now) in self.pressed.clone() {
+ let _ = self.observe("keyup", now + average, json!({"code":key,"estimated":true}));
+ }
+ let start = self
+ .events
+ .iter()
+ .find(|e| e.kind == "timer" && e.data["event"] == "start")
+ .map(|e| e.now);
+ let end = self
+ .events
+ .iter()
+ .find(|e| e.kind == "timer" && e.data["event"] == "end")
+ .map(|e| e.now);
+ let last_pre = self
+ .events
+ .iter()
+ .enumerate()
+ .filter(|(_, e)| e.kind == "keydown" && start.is_some_and(|start| e.now < start))
+ .map(|(i, _)| i)
+ .next_back();
+ let post_codes: Vec<_> = self
+ .events
+ .iter()
+ .filter(|e| e.kind == "keydown" && end.is_some_and(|end| e.now > end))
+ .map(|e| e.data["code"].clone())
+ .collect();
+ let mut index = 0;
+ self.events.retain(|e| {
+ let current = index;
+ index += 1;
+ if let Some(start) = start
+ && e.now < start
+ && (e.kind == "keyup" || (e.kind == "keydown" && Some(current) != last_pre))
+ {
+ return false;
+ }
+ if let Some(end) = end
+ && e.now > end
+ && (e.kind == "input"
+ || e.kind == "keydown"
+ || (e.kind == "keyup" && post_codes.contains(&e.data["code"])))
+ {
+ return false;
+ }
+ true
+ });
+ }
fn origin(&self) -> f64 {
self.events
.iter()
diff --git a/crates/typing-engine/tests/recordings.rs b/crates/typing-engine/tests/recordings.rs
new file mode 100644
index 000000000..5d83761cc
--- /dev/null
+++ b/crates/typing-engine/tests/recordings.rs
@@ -0,0 +1,129 @@
+use serde_json::{Value, json};
+use typing_engine::{Command, Session, Text};
+
+fn input_neutral(mut event: Value) -> Value {
+ for key in ["inputValue", "data"] {
+ if let Some(text) = event["data"][key].as_str() {
+ event["data"][key] = json!(Text::from(text));
+ }
+ }
+ event
+}
+
+#[test]
+fn all_original_browser_recordings_match_input_events() {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../packages/typing-fixtures/reference/keystrokes");
+ for file in std::fs::read_dir(root).unwrap() {
+ let path = file.unwrap().path();
+ if path.extension().is_none_or(|ext| ext != "json") {
+ continue;
+ }
+ let fixture: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
+ let words = fixture["eventLog"]["context"]["targetWords"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|word| Text::from(word.as_str().unwrap()))
+ .collect();
+ let mut session = Session::new(
+ serde_json::from_value(fixture["config"].clone()).unwrap(),
+ words,
+ )
+ .unwrap();
+ for event in fixture["eventLog"]["events"].as_array().unwrap() {
+ let now = event["testMs"].as_f64().unwrap();
+ let data = &event["data"];
+ match event["type"].as_str().unwrap() {
+ "input" => {
+ let command = if data["inputType"] == "insertText" {
+ json!({"op":"insert","data":Text::from(data["data"].as_str().unwrap()),"now":now,
+ "automatic":data.get("automatic"),"isCompositionEnding":data.get("isCompositionEnding")})
+ } else {
+ json!({"op":"delete","kind":data["inputType"],"now":now})
+ };
+ session
+ .apply(serde_json::from_value(command).unwrap())
+ .unwrap();
+ }
+ "timer" if data["event"] == "start" => {
+ session.apply(Command::Start { now, date: 0.0 }).unwrap();
+ }
+ "timer" if data["event"] == "end" => {
+ session
+ .apply(Command::Finish {
+ now,
+ date: 0.0,
+ reason: None,
+ })
+ .unwrap();
+ }
+ kind => {
+ session
+ .apply(Command::Observe {
+ now,
+ event_type: kind.into(),
+ event_data: data.clone(),
+ })
+ .unwrap();
+ }
+ }
+ }
+ let actual = session.log()["events"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .filter(|event| event["type"] == "input")
+ .cloned()
+ .collect::>();
+ let expected = fixture["eventLog"]["events"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .filter(|event| event["type"] == "input")
+ .cloned()
+ .map(input_neutral)
+ .collect::>();
+ assert_eq!(actual.len(), expected.len(), "{}", path.display());
+ for (index, (actual, expected)) in actual.iter().zip(&expected).enumerate() {
+ assert_eq!(
+ actual["type"],
+ expected["type"],
+ "{} event {index}",
+ path.display()
+ );
+ assert_eq!(
+ actual["testMs"].as_f64(),
+ expected["testMs"].as_f64(),
+ "{} event {index}",
+ path.display()
+ );
+ assert_eq!(
+ actual["data"],
+ expected["data"],
+ "{} event {index}",
+ path.display()
+ );
+ }
+ }
+}
+
+#[test]
+fn absent_physical_observations_do_not_create_key_durations() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":1})).unwrap(),
+ vec![Text::from("a")],
+ )
+ .unwrap();
+ session
+ .apply(serde_json::from_value(json!({"op":"insert","data":[97],"now":0})).unwrap())
+ .unwrap();
+ session.apply(Command::Finalize { now: 10.0 }).unwrap();
+ assert!(
+ session.log()["events"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .all(|event| event["type"] != "keyup" && event["type"] != "keydown")
+ );
+}
diff --git a/crates/typing-engine/tests/slice.rs b/crates/typing-engine/tests/slice.rs
index a08a26cc3..350b0f25b 100644
--- a/crates/typing-engine/tests/slice.rs
+++ b/crates/typing-engine/tests/slice.rs
@@ -43,6 +43,7 @@ fn finite_slice_matches_frozen_intermediate_oracle() {
"delete-error-letter_hard",
"delete-error-word",
"delete-error-word_hard",
+ "physical-observations",
] {
let fixture: Fixture = serde_json::from_slice(
&std::fs::read(root.join(format!("commands/{name}.json"))).unwrap(),
diff --git a/packages/typing-core/scripts/capture-rust-oracle.ts b/packages/typing-core/scripts/capture-rust-oracle.ts
index 67a62d533..169ea2d8c 100644
--- a/packages/typing-core/scripts/capture-rust-oracle.ts
+++ b/packages/typing-core/scripts/capture-rust-oracle.ts
@@ -4,9 +4,11 @@ import { readCommandFixture, runOracle } from "./rust-oracle";
const root = resolve(import.meta.dirname, "../../typing-fixtures");
// Capture only NEW traces. Deliberately no overwrite/update flag.
-for (const name of readdirSync(resolve(root, "commands")).filter((entry) =>
- entry.endsWith(".json"),
-)) {
+const requested = process.argv.slice(2);
+for (const name of (requested.length > 0
+ ? requested
+ : readdirSync(resolve(root, "commands"))
+).filter((entry) => entry.endsWith(".json"))) {
const fixture = readCommandFixture(resolve(root, "commands", name));
const output = await runOracle(fixture);
writeFileSync(
diff --git a/packages/typing-core/scripts/rust-oracle.ts b/packages/typing-core/scripts/rust-oracle.ts
index 1d3b330f4..23c6b6ddc 100644
--- a/packages/typing-core/scripts/rust-oracle.ts
+++ b/packages/typing-core/scripts/rust-oracle.ts
@@ -19,7 +19,8 @@ export type OracleCommand = {
| "end"
| "abandon"
| "reset"
- | "observe";
+ | "observe"
+ | "finalize";
now: number;
date?: number;
data?: Text;
@@ -112,6 +113,10 @@ export async function runOracle(fixture: CommandFixture): Promise {
case "reset":
session.reset();
break;
+ case "finalize":
+ session.recorder.forceReleaseAllKeys();
+ session.recorder.cleanupData();
+ break;
case "observe":
if (
command.eventType === undefined ||
diff --git a/packages/typing-fixtures/commands.schema.json b/packages/typing-fixtures/commands.schema.json
index b8adf517e..b0939974c 100644
--- a/packages/typing-fixtures/commands.schema.json
+++ b/packages/typing-fixtures/commands.schema.json
@@ -35,6 +35,7 @@
"end",
"abandon",
"reset",
+ "finalize",
"observe"
]
},
diff --git a/packages/typing-fixtures/commands/physical-observations.json b/packages/typing-fixtures/commands/physical-observations.json
new file mode 100644
index 000000000..c06bbbef3
--- /dev/null
+++ b/packages/typing-fixtures/commands/physical-observations.json
@@ -0,0 +1,113 @@
+{
+ "name": "physical-observations",
+ "config": {
+ "mode": "words",
+ "words": 1
+ },
+ "words": [[97, 98, 99]],
+ "commands": [
+ {
+ "op": "observe",
+ "now": 900,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 910,
+ "eventType": "keyup",
+ "eventData": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 950,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "op": "start",
+ "now": 1000,
+ "date": 5000
+ },
+ {
+ "op": "observe",
+ "now": 1020,
+ "eventType": "keyup",
+ "eventData": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1100,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1120,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1180,
+ "eventType": "keyup",
+ "eventData": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1200,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "ShiftLeft"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1250,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "NoCode"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1260,
+ "eventType": "keydown",
+ "eventData": {
+ "code": "NoCode"
+ }
+ },
+ {
+ "op": "observe",
+ "now": 1270,
+ "eventType": "keyup",
+ "eventData": {
+ "code": "NoCode"
+ }
+ },
+ {
+ "op": "insert",
+ "data": [97, 98, 99],
+ "now": 1300,
+ "date": 5300
+ },
+ {
+ "op": "finalize",
+ "now": 1400
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/physical-observations.json b/packages/typing-fixtures/oracle/physical-observations.json
new file mode 100644
index 000000000..d49f79c78
--- /dev/null
+++ b/packages/typing-fixtures/oracle/physical-observations.json
@@ -0,0 +1,1139 @@
+{
+ "name": "physical-observations",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": 0,
+ "data": {
+ "code": "KeyX"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": 0,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10,
+ "data": {
+ "code": "KeyX"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": false,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": 0,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 10,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 50,
+ "data": {
+ "code": "KeyA"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 250,
+ "data": {
+ "code": "NoCode0"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 250,
+ "data": {
+ "code": "NoCode0"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 260,
+ "data": {
+ "code": "NoCode1"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 250,
+ "data": {
+ "code": "NoCode0"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 260,
+ "data": {
+ "code": "NoCode1"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 270,
+ "data": {
+ "code": "NoCode1"
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [97, 98, 99],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -100,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": -90,
+ "data": {
+ "code": "KeyX"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 250,
+ "data": {
+ "code": "NoCode0"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 260,
+ "data": {
+ "code": "NoCode1"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 270,
+ "data": {
+ "code": "NoCode1"
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [98],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 98],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [99],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [97, 98, 99],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 300,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 5300
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [97, 98, 99],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "keydown",
+ "testMs": -50,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 5000
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 20,
+ "data": {
+ "code": "KeyA"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 100,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 120,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 180,
+ "data": {
+ "code": "KeyB"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 250,
+ "data": {
+ "code": "NoCode0"
+ }
+ },
+ {
+ "type": "keydown",
+ "testMs": 260,
+ "data": {
+ "code": "NoCode1"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 270,
+ "data": {
+ "code": "NoCode1"
+ }
+ },
+ {
+ "type": "keyup",
+ "testMs": 280,
+ "data": {
+ "code": "NoCode0",
+ "estimated": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [98],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 98],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 300,
+ "data": {
+ "inputType": "insertText",
+ "data": [99],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [97, 98, 99],
+ "lastWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 300,
+ "data": {
+ "event": "end",
+ "timer": 0,
+ "date": 5300
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 98, 99]],
+ "mode": "words",
+ "mode2": "1",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
From 72ff8da2ce04a3961ffb2b26ac2ef7cd72b3a683 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:09:55 +0530
Subject: [PATCH 11/93] feat(engine): derive legacy score and timing fields
from event logs
---
Cargo.toml | 5 +-
crates/typing-engine/src/lib.rs | 1 +
crates/typing-engine/src/stats.rs | 412 +++++++++++++++++++++++
crates/typing-engine/tests/statistics.rs | 55 +++
4 files changed, 472 insertions(+), 1 deletion(-)
create mode 100644 crates/typing-engine/src/stats.rs
create mode 100644 crates/typing-engine/tests/statistics.rs
diff --git a/Cargo.toml b/Cargo.toml
index a9ce7534a..657027a0b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,10 @@ repository = "https://github.com/voltcrash/oxytype"
[workspace.dependencies]
serde = { version = "1.0.229", features = ["derive"] }
-serde_json = { version = "1.0.151", features = ["preserve_order"] }
+serde_json = { version = "1.0.151", features = [
+ "preserve_order",
+ "float_roundtrip",
+] }
wasm-bindgen = "=0.2.129"
ts-rs = { version = "=12.0.1", features = ["serde-json-impl"] }
diff --git a/crates/typing-engine/src/lib.rs b/crates/typing-engine/src/lib.rs
index e9ff18836..185ab9fc1 100644
--- a/crates/typing-engine/src/lib.rs
+++ b/crates/typing-engine/src/lib.rs
@@ -1,5 +1,6 @@
//! Platform-independent typing rules. Hosts provide content, observations and time.
pub mod session;
+pub mod stats;
pub mod text;
pub use session::{Command, Config, Session};
diff --git a/crates/typing-engine/src/stats.rs b/crates/typing-engine/src/stats.rs
new file mode 100644
index 000000000..eb13aa787
--- /dev/null
+++ b/crates/typing-engine/src/stats.rs
@@ -0,0 +1,412 @@
+//! Legacy event-log derivation. Text offsets/counts remain UTF-16 units.
+use crate::{
+ Text, js_round, round2,
+ session::Event,
+ text::{is_space, js_whitespace},
+};
+use serde::{Deserialize, Serialize};
+use serde_json::{Value, json};
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Context {
+ pub target_words: Vec,
+ pub mode: String,
+ pub mode2: String,
+ #[serde(default)]
+ pub bailed_out: bool,
+ #[serde(default)]
+ pub korean_status: bool,
+ pub custom_text_limit_mode: Option,
+ pub custom_text_limit_value: Option,
+}
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Log {
+ pub version: u32,
+ pub events: Vec,
+ pub context: Context,
+}
+
+pub fn text(value: &Value) -> Text {
+ if let Some(value) = value.as_str() {
+ Text::from(value)
+ } else {
+ serde_json::from_value(value.clone()).unwrap_or_default()
+ }
+}
+pub fn word_index(event: &Event) -> Option {
+ event.data["wordIndex"].as_u64().map(|v| v as usize)
+}
+fn is_insert(event: &Event) -> bool {
+ event.kind == "input"
+ && (event.data["inputType"] == "insertText"
+ || event.data["inputType"] == "insertCompositionText")
+}
+pub fn input(events: &[&Event]) -> Text {
+ let Some(event) = events.iter().rev().find(|event| event.kind == "input") else {
+ return Text::default();
+ };
+ let mut value = text(&event.data["inputValue"]);
+ if event.data["inputType"] == "insertText"
+ && text(&event.data["data"]).0 == [32]
+ && event.data["lastWord"] == true
+ && event.data["commitsWord"] == true
+ && event.data["correct"] == false
+ {
+ while value.0.last().is_some_and(|c| js_whitespace(*c)) {
+ value.0.pop();
+ }
+ }
+ value
+}
+fn per_word(log: &Log, limit: Option) -> Vec<(usize, Vec<&Event>)> {
+ let mut words: Vec<(usize, Vec<&Event>)> = vec![];
+ for event in &log.events {
+ if limit.is_some_and(|limit| event.test_ms > limit) {
+ break;
+ }
+ if let Some(index) = word_index(event) {
+ if let Some((_, events)) = words.iter_mut().find(|(i, _)| *i == index) {
+ events.push(event);
+ } else {
+ words.push((index, vec![event]));
+ }
+ }
+ }
+ words
+}
+fn active_index(words: &[(usize, Vec<&Event>)]) -> usize {
+ let last = words
+ .iter()
+ .filter(|(_, events)| !input(events).is_empty())
+ .max_by_key(|(i, _)| *i);
+ let Some((index, events)) = last else {
+ return 0;
+ };
+ let space = events
+ .last()
+ .is_some_and(|e| e.data["inputType"] == "insertText" && text(&e.data["data"]).0 == [32]);
+ *index + usize::from(space)
+}
+pub fn timed(log: &Log) -> bool {
+ log.context.mode == "time"
+ || (log.context.mode == "words" && log.context.mode2 == "0")
+ || (log.context.mode == "custom"
+ && (log.context.custom_text_limit_mode.as_deref() == Some("time")
+ || log.context.custom_text_limit_value == Some(0.0)))
+}
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Chars {
+ pub all_correct: usize,
+ pub correct_word: usize,
+ pub incorrect: usize,
+ pub extra: usize,
+ pub missed: usize,
+}
+pub fn count_chars(input: &Text, target: &Text, partial: bool) -> Chars {
+ let correct = input == target;
+ let prefix = target.0.starts_with(&input.0);
+ let mut chars = Chars::default();
+ for index in 0..input.len().max(target.len()) {
+ let a = input.0.get(index);
+ let b = target.0.get(index);
+ if a == b {
+ if b == Some(&32) && !correct {
+ chars.extra += 1;
+ } else {
+ chars.all_correct += 1;
+ }
+ if correct || (partial && prefix) {
+ chars.correct_word += 1;
+ }
+ } else if a.is_none() {
+ if !partial {
+ chars.missed += 1;
+ }
+ } else if b.is_none() || (b == Some(&32) && a != Some(&32) && !input.0.contains(&32)) {
+ chars.extra += 1;
+ } else {
+ chars.incorrect += 1;
+ }
+ }
+ chars
+}
+pub fn chars(log: &Log, partial: bool, limit: Option) -> Chars {
+ let words = per_word(log, limit);
+ let active = active_index(&words);
+ let mut total = Chars::default();
+ for (index, events) in words {
+ let mut typed = input(&events);
+ for unit in &mut typed.0 {
+ if is_space(*unit) {
+ *unit = 32;
+ }
+ }
+ let target = log.context.target_words.get(index).unwrap_or(&typed);
+ let counts = count_chars(
+ &typed,
+ target,
+ index == active && (partial || timed(log) || log.context.bailed_out),
+ );
+ total.all_correct += counts.all_correct;
+ total.correct_word += counts.correct_word;
+ total.incorrect += counts.incorrect;
+ total.extra += counts.extra;
+ total.missed += counts.missed;
+ if index == active {
+ break;
+ }
+ }
+ total
+}
+pub fn wpm(chars: usize, seconds: f64) -> f64 {
+ if seconds <= 0.0 {
+ 0.0
+ } else {
+ chars as f64 / 5.0 / (seconds / 60.0)
+ }
+}
+pub fn accuracy(log: &Log, limit: Option) -> f64 {
+ let mut correct = 0;
+ let mut total = 0;
+ for event in &log.events {
+ if limit.is_some_and(|limit| event.test_ms > limit) {
+ break;
+ }
+ if event.kind == "input" && event.data.get("correct").is_some() {
+ total += 1;
+ if event.data["correct"] == true {
+ correct += 1;
+ }
+ }
+ }
+ if total == 0 {
+ 0.0
+ } else {
+ correct as f64 / total as f64 * 100.0
+ }
+}
+fn end(log: &Log) -> f64 {
+ log.events
+ .iter()
+ .rev()
+ .find(|event| event.kind == "timer" && event.data["event"] == "end")
+ .map(|event| event.test_ms)
+ .unwrap_or(0.0)
+}
+fn raw_last_key_to_end(log: &Log) -> f64 {
+ let last = log
+ .events
+ .iter()
+ .rev()
+ .find(|event| event.kind == "keydown");
+ last.map(|event| round2((end(log) - event.test_ms).max(0.0)))
+ .unwrap_or(0.0)
+}
+fn adjusted_end(log: &Log) -> f64 {
+ let mut end = end(log);
+ if log.context.mode == "zen" || log.context.bailed_out {
+ let last = raw_last_key_to_end(log);
+ if last < 7000.0 {
+ end -= last;
+ }
+ }
+ end
+}
+pub fn duration(log: &Log) -> f64 {
+ let end = adjusted_end(log);
+ if log.context.mode == "custom" {
+ end
+ } else {
+ round2(end / 1000.0) * 1000.0
+ }
+}
+pub fn boundaries(log: &Log) -> Vec {
+ if !log
+ .events
+ .iter()
+ .any(|event| event.kind == "timer" && event.data["event"] == "end")
+ {
+ return vec![];
+ }
+ let end = adjusted_end(log);
+ let ticks = (end / 1000.0).floor() as usize;
+ let mut boundaries: Vec<_> = (1..=ticks).map(|i| i as f64 * 1000.0).collect();
+ if !timed(log) && js_round(round2(end / 1000.0) % 1.0) >= 0.5 {
+ boundaries.push(end);
+ }
+ boundaries
+}
+fn count_intervals(log: &Log, predicate: impl Fn(&Event) -> bool) -> Vec {
+ let mut index = 0;
+ let mut counts = vec![];
+ for boundary in boundaries(log) {
+ let mut count = 0;
+ while let Some(event) = log.events.get(index) {
+ if event.test_ms > boundary {
+ break;
+ }
+ if predicate(event) {
+ count += 1;
+ }
+ index += 1;
+ }
+ counts.push(count);
+ }
+ counts
+}
+pub fn keypresses_per_second(log: &Log) -> Vec {
+ count_intervals(log, |event| {
+ event.kind == "input" && event.data["inputType"] == "insertText"
+ })
+}
+pub fn burst_history(log: &Log) -> Vec {
+ let counts = keypresses_per_second(log);
+ let mut previous = 0.0;
+ boundaries(log)
+ .iter()
+ .zip(counts)
+ .map(|(boundary, count)| {
+ let seconds = (boundary - previous) / 1000.0;
+ previous = *boundary;
+ js_round(wpm(count, seconds))
+ })
+ .collect()
+}
+pub fn speed_history(log: &Log, raw: bool) -> Vec {
+ boundaries(log)
+ .iter()
+ .map(|boundary| {
+ let chars = chars(log, true, Some(*boundary));
+ js_round(wpm(
+ if raw {
+ chars.all_correct + chars.incorrect + chars.extra
+ } else {
+ chars.correct_word
+ },
+ boundary / 1000.0,
+ ))
+ })
+ .collect()
+}
+pub fn afk(log: &Log) -> usize {
+ count_intervals(log, |e| e.kind == "keydown" || e.kind == "input")
+ .iter()
+ .filter(|count| **count == 0)
+ .count()
+}
+pub fn incomplete_seconds(log: &Log) -> f64 {
+ round2(duration(log) / 1000.0 - afk(log) as f64).max(0.0)
+}
+pub fn consistency(values: &[f64]) -> f64 {
+ if values.is_empty() {
+ return 0.0;
+ }
+ let mean = values.iter().sum::() / values.len() as f64;
+ let deviation = (values
+ .iter()
+ .map(|value| (value - mean).powi(2))
+ .sum::()
+ / values.len() as f64)
+ .sqrt();
+ let cov = deviation / mean;
+ let result = round2(100.0 * (1.0 - (cov + cov.powi(3) / 3.0 + cov.powi(5) / 5.0).tanh()));
+ if result.is_nan() { 0.0 } else { result }
+}
+pub fn word_burst(log: &Log, index: usize, now: f64) -> f64 {
+ let events: Vec<_> = log
+ .events
+ .iter()
+ .filter(|e| word_index(e) == Some(index))
+ .collect();
+ let input = input(&events);
+ let mut length = input.len();
+ if !matches!(input.0.last(), Some(32 | 10)) {
+ length += 1;
+ }
+ let mut first = None;
+ let mut last = None;
+ for event in events {
+ if event.kind == "composition" && event.data["event"] == "start" && first.is_none() {
+ first = Some(event.test_ms);
+ }
+ if is_insert(event) {
+ if event.data["charIndex"] == 0 && first.is_none() {
+ first = Some(event.test_ms);
+ }
+ if first.is_some() {
+ last = Some(event.test_ms);
+ }
+ }
+ }
+ let Some(first) = first else {
+ return 0.0;
+ };
+ if input.is_empty() {
+ return 0.0;
+ }
+ let seconds = (last.filter(|last| *last >= first).unwrap_or(now) - first) / 1000.0;
+ if seconds <= 0.0 {
+ f64::INFINITY
+ } else {
+ js_round(wpm(length, seconds))
+ }
+}
+pub fn summary(log: &Log) -> Value {
+ let counts = chars(log, false, None);
+ let seconds = duration(log) / 1000.0;
+ let burst = burst_history(log);
+ let speed = speed_history(log, false);
+ let errors = count_intervals(log, |event| {
+ event.kind == "input"
+ && event.data["inputType"] == "insertText"
+ && event.data["correct"] == false
+ });
+ let mut spacing = vec![];
+ let mut last = None;
+ let mut durations: Vec = vec![];
+ let mut held: Vec<(String, f64, usize)> = vec![];
+ let mut overlap = 0.0;
+ let mut overlap_start = None;
+ let first = log
+ .events
+ .iter()
+ .find(|event| event.kind == "keydown")
+ .map(|event| event.test_ms.max(0.0))
+ .unwrap_or(0.0);
+ for event in &log.events {
+ let code = event.data["code"].as_str().unwrap_or_default().to_owned();
+ if event.kind == "keydown" {
+ if let Some(previous) = last {
+ spacing.push(event.test_ms - previous);
+ }
+ last = Some(event.test_ms.max(0.0));
+ if let Some(index) = held.iter().position(|(key, _, _)| key == &code) {
+ held.remove(index);
+ }
+ held.push((code, event.test_ms, durations.len()));
+ durations.push(0.0);
+ if overlap_start.is_none() && held.len() > 1 {
+ overlap_start = Some(event.test_ms);
+ }
+ } else if event.kind == "keyup" {
+ if let Some(index) = held.iter().position(|(key, _, _)| key == &code) {
+ let (_, time, index) = held.remove(index);
+ durations[index] = event.test_ms - time;
+ }
+ if let Some(start) = overlap_start
+ && held.len() == 1
+ {
+ overlap += event.test_ms - start;
+ overlap_start = None;
+ }
+ }
+ }
+ json!({"wpm":round2(wpm(counts.correct_word,seconds)),"rawWpm":round2(wpm(counts.all_correct+counts.incorrect+counts.extra,seconds)),
+ "charStats":[counts.correct_word,counts.incorrect,counts.extra,counts.missed],"charTotal":counts.all_correct+counts.incorrect+counts.extra,
+ "acc":round2(accuracy(log,None)),"testDuration":seconds,"lastKeyToEnd":if log.context.mode=="zen" {0.0} else {raw_last_key_to_end(log)},
+ "startToFirstKey":if log.context.mode=="zen" {0.0} else {round2(first)},"afkDuration":afk(log),
+ "consistency":consistency(&burst),"wpmConsistency":consistency(&speed),"keyConsistency":consistency(&spacing[..spacing.len().saturating_sub(1)]),
+ "chartData":{"wpm":speed,"burst":burst,"err":errors},"keySpacing":spacing,"keyDuration":durations,"keyOverlap":round2(overlap)})
+}
diff --git a/crates/typing-engine/tests/statistics.rs b/crates/typing-engine/tests/statistics.rs
new file mode 100644
index 000000000..b53ae420e
--- /dev/null
+++ b/crates/typing-engine/tests/statistics.rs
@@ -0,0 +1,55 @@
+use serde_json::{Value, json};
+use typing_engine::{
+ Text,
+ stats::{Log, summary},
+};
+
+fn equal(a: &Value, b: &Value, path: &str) {
+ match (a, b) {
+ (Value::Number(a), Value::Number(b)) => assert_eq!(a.as_f64(), b.as_f64(), "{path}"),
+ (Value::Array(a), Value::Array(b)) => {
+ assert_eq!(a.len(), b.len(), "{path}");
+ for (index, (a, b)) in a.iter().zip(b).enumerate() {
+ equal(a, b, &format!("{path}[{index}]"));
+ }
+ }
+ (Value::Object(a), Value::Object(b)) => {
+ assert_eq!(a.len(), b.len(), "{path}");
+ for (key, a) in a {
+ equal(a, &b[key], &format!("{path}.{key}"));
+ }
+ }
+ _ => assert_eq!(a, b, "{path}"),
+ }
+}
+
+#[test]
+fn summary_fields_match_all_unchanged_browser_results_exactly() {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../packages/typing-fixtures/reference/keystrokes");
+ for file in std::fs::read_dir(root).unwrap() {
+ let path = file.unwrap().path();
+ if path.extension().is_none_or(|ext| ext != "json") {
+ continue;
+ }
+ let fixture: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
+ let mut log = fixture["eventLog"].clone();
+ log["context"]["targetWords"] = json!(
+ log["context"]["targetWords"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|word| Text::from(word.as_str().unwrap()))
+ .collect::>()
+ );
+ let log: Log = serde_json::from_value(log).unwrap();
+ let actual = summary(&log);
+ for (key, actual) in actual.as_object().unwrap() {
+ equal(
+ actual,
+ &fixture["completedEvent"][key],
+ &format!("{}.{key}", path.display()),
+ );
+ }
+ }
+}
From 936cddaca6c520af43450d6a968c9b67c60ec0bd Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:09:56 +0530
Subject: [PATCH 12/93] feat(engine): preserve timer stalls minimum accuracy
speed and burst failures
---
crates/typing-engine/src/session.rs | 69 +++++
crates/typing-engine/tests/slice.rs | 5 +
docs/rust-engine/progress.md | 5 +-
.../typing-fixtures/commands/min-burst.json | 25 ++
.../commands/time-stall-finish.json | 26 ++
.../typing-fixtures/commands/time-zero.json | 26 ++
.../commands/timer-min-accuracy.json | 24 ++
.../commands/timer-min-speed.json | 21 ++
.../typing-fixtures/oracle/min-burst.json | 142 +++++++++
.../oracle/time-stall-finish.json | 212 +++++++++++++
.../typing-fixtures/oracle/time-zero.json | 239 ++++++++++++++
.../oracle/timer-min-accuracy.json | 114 +++++++
.../oracle/timer-min-speed.json | 293 ++++++++++++++++++
13 files changed, 1200 insertions(+), 1 deletion(-)
create mode 100644 packages/typing-fixtures/commands/min-burst.json
create mode 100644 packages/typing-fixtures/commands/time-stall-finish.json
create mode 100644 packages/typing-fixtures/commands/time-zero.json
create mode 100644 packages/typing-fixtures/commands/timer-min-accuracy.json
create mode 100644 packages/typing-fixtures/commands/timer-min-speed.json
create mode 100644 packages/typing-fixtures/oracle/min-burst.json
create mode 100644 packages/typing-fixtures/oracle/time-stall-finish.json
create mode 100644 packages/typing-fixtures/oracle/time-zero.json
create mode 100644 packages/typing-fixtures/oracle/timer-min-accuracy.json
create mode 100644 packages/typing-fixtures/oracle/timer-min-speed.json
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
index 2a82eaf9d..7449ba42a 100644
--- a/crates/typing-engine/src/session.rs
+++ b/crates/typing-engine/src/session.rs
@@ -24,6 +24,12 @@ pub struct Config {
pub blind_mode: bool,
pub code_unindent_on_backspace: bool,
pub funbox: Vec,
+ pub min_burst: String,
+ pub min_burst_custom_speed: f64,
+ pub min_wpm: String,
+ pub min_wpm_custom_speed: f64,
+ pub min_acc: String,
+ pub min_acc_custom: f64,
}
impl Default for Config {
@@ -43,6 +49,12 @@ impl Default for Config {
blind_mode: false,
code_unindent_on_backspace: false,
funbox: vec![],
+ min_burst: "off".into(),
+ min_burst_custom_speed: 100.0,
+ min_wpm: "off".into(),
+ min_wpm_custom_speed: 100.0,
+ min_acc: "off".into(),
+ min_acc_custom: 90.0,
}
}
}
@@ -447,6 +459,30 @@ impl Session {
self.end(now, date, Some("difficulty".into()));
return;
}
+ if advance && self.config.min_burst != "off" {
+ let log: crate::stats::Log =
+ serde_json::from_value(self.log()).expect("Engine log is valid");
+ let burst = crate::stats::word_burst(&log, self.index, now - self.start.unwrap_or(now));
+ let length = if self.config.mode == "zen" {
+ input.len() + 1
+ } else {
+ target.len()
+ };
+ let flex = self.config.min_burst_custom_speed.min(
+ (self.config.min_burst_custom_speed * 1.03_f64.powf(-2.0 * (length as f64 - 3.0)))
+ .floor(),
+ );
+ if burst
+ < (if self.config.min_burst == "flex" {
+ flex
+ } else {
+ self.config.min_burst_custom_speed
+ })
+ {
+ self.end(now, date, Some("min burst".into()));
+ return;
+ }
+ }
let finite = self.config.mode != "time"
&& self.config.mode != "zen"
&& !(self.config.mode == "words" && self.config.words == 0);
@@ -604,6 +640,39 @@ impl Session {
data["drift"] = json!(drift);
}
self.record("timer", now, data);
+ if self.ticks == due
+ && (self.config.min_wpm == "custom" || self.config.min_acc == "custom")
+ {
+ let log: crate::stats::Log =
+ serde_json::from_value(self.log()).expect("Engine log is valid");
+ let chars = crate::stats::chars(&log, true, None);
+ let wpm = crate::js_round(crate::stats::wpm(
+ chars.correct_word,
+ (now - start) / 1000.0,
+ ));
+ let has_input = self
+ .events
+ .iter()
+ .any(|e| e.kind == "input" && e.data.get("correct").is_some());
+ let accuracy = if has_input {
+ crate::stats::accuracy(&log, None)
+ } else {
+ 100.0
+ };
+ let reason = if self.config.min_wpm == "custom"
+ && wpm < self.config.min_wpm_custom_speed
+ && self.index > 3
+ {
+ Some("min speed")
+ } else if self.config.min_acc == "custom" && accuracy < self.config.min_acc_custom {
+ Some("min accuracy")
+ } else {
+ None
+ };
+ if let Some(reason) = reason {
+ self.end(now, date, Some(reason.into()));
+ }
+ }
if self.config.mode == "time"
&& self.config.time > 0.0
&& f64::from(self.ticks) >= self.config.time
diff --git a/crates/typing-engine/tests/slice.rs b/crates/typing-engine/tests/slice.rs
index 350b0f25b..814c3aa30 100644
--- a/crates/typing-engine/tests/slice.rs
+++ b/crates/typing-engine/tests/slice.rs
@@ -44,6 +44,11 @@ fn finite_slice_matches_frozen_intermediate_oracle() {
"delete-error-word",
"delete-error-word_hard",
"physical-observations",
+ "timer-min-accuracy",
+ "timer-min-speed",
+ "min-burst",
+ "time-stall-finish",
+ "time-zero",
] {
let fixture: Fixture = serde_json::from_slice(
&std::fs::read(root.join(format!("commands/{name}.json"))).unwrap(),
diff --git a/docs/rust-engine/progress.md b/docs/rust-engine/progress.md
index e712bc775..f52d8b4b2 100644
--- a/docs/rust-engine/progress.md
+++ b/docs/rust-engine/progress.md
@@ -21,7 +21,7 @@ Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen o
| 1.1 Inventory / baselines | Complete | See inventory.md, features.md, baseline.md and raw measurements. |
| 1.2 Contracts / fixtures | Complete | 13 command traces, 15 oracle/integrity tests; contract and classified gaps. |
| 1.3 Native / browser proof | Complete | Native 7 traces; Cloudflare real-browser finite slice; MIME/CSP, lifecycle, boundary benchmark. Budgets frozen. |
-| 1.4 Input / lifecycle / timing | Active | Remaining input penalties, code, physical observations, browser differences and timing. |
+| 1.4 Input / lifecycle / timing | Active | Input penalties/code and physical recording now match 14 traces and 8 browser recordings. Browser differences/timing failures still open. |
| 1.5 Generation / configuration | Pending | Requires contract and proof. |
| 1.6 Events / results / replay | Pending | Requires input and generation. |
| 1.7 Binding / engine gate | Pending | Full three-path parity and budgets. |
@@ -46,6 +46,9 @@ Phase 1.1 and 1.2 gates passed: inventory/baselines, lossless contract, frozen o
- Phase 1.3: boundary benchmark/type-aware lint passed; proof-performance.json and performance-budgets.md record limits and evidence.
- Early failed browser proof correctly exposed CSP; fixed and retested. Initial build raced workspace dependency installation; reran after install completed.
+- Phase 1.4 partial: native input logs match all eight original browser recordings. All 14 command traces (including physical repeat/NoCode/estimated cleanup) match intermediate frames.
+- Native Clippy passes. Pure event statistics are being ported as prerequisites for min-speed/min-burst timing rules; full result gate remains pending.
+
## Decisions
- User's three-stage PR structure overrides the plan's suggested per-phase PRs.
diff --git a/packages/typing-fixtures/commands/min-burst.json b/packages/typing-fixtures/commands/min-burst.json
new file mode 100644
index 000000000..2e74a8d46
--- /dev/null
+++ b/packages/typing-fixtures/commands/min-burst.json
@@ -0,0 +1,25 @@
+{
+ "name": "min-burst",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "minBurst": "fixed",
+ "minBurstCustomSpeed": 100
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "now": 0,
+ "data": [111]
+ },
+ {
+ "op": "insert",
+ "now": 10000,
+ "data": [110, 101, 32]
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/time-stall-finish.json b/packages/typing-fixtures/commands/time-stall-finish.json
new file mode 100644
index 000000000..91d4cb710
--- /dev/null
+++ b/packages/typing-fixtures/commands/time-stall-finish.json
@@ -0,0 +1,26 @@
+{
+ "name": "time-stall-finish",
+ "config": {
+ "mode": "time",
+ "time": 3
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "now": 0,
+ "data": [111]
+ },
+ {
+ "op": "advance",
+ "now": 5000
+ },
+ {
+ "op": "advance",
+ "now": 6000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/time-zero.json b/packages/typing-fixtures/commands/time-zero.json
new file mode 100644
index 000000000..1795b0de7
--- /dev/null
+++ b/packages/typing-fixtures/commands/time-zero.json
@@ -0,0 +1,26 @@
+{
+ "name": "time-zero",
+ "config": {
+ "mode": "time",
+ "time": 0
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "now": 0,
+ "data": [111]
+ },
+ {
+ "op": "advance",
+ "now": 5000
+ },
+ {
+ "op": "end",
+ "now": 6000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/timer-min-accuracy.json b/packages/typing-fixtures/commands/timer-min-accuracy.json
new file mode 100644
index 000000000..b80d235a8
--- /dev/null
+++ b/packages/typing-fixtures/commands/timer-min-accuracy.json
@@ -0,0 +1,24 @@
+{
+ "name": "timer-min-accuracy",
+ "config": {
+ "mode": "words",
+ "words": 2,
+ "minAcc": "custom",
+ "minAccCustom": 90
+ },
+ "words": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "commands": [
+ {
+ "op": "insert",
+ "now": 0,
+ "data": [120]
+ },
+ {
+ "op": "advance",
+ "now": 1000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/commands/timer-min-speed.json b/packages/typing-fixtures/commands/timer-min-speed.json
new file mode 100644
index 000000000..22dfa8225
--- /dev/null
+++ b/packages/typing-fixtures/commands/timer-min-speed.json
@@ -0,0 +1,21 @@
+{
+ "name": "timer-min-speed",
+ "config": {
+ "mode": "words",
+ "words": 5,
+ "minWpm": "custom",
+ "minWpmCustomSpeed": 100
+ },
+ "words": [[97, 32], [98, 32], [99, 32], [100, 32], [101]],
+ "commands": [
+ {
+ "op": "insert",
+ "now": 0,
+ "data": [97, 32, 98, 32, 99, 32, 100, 32]
+ },
+ {
+ "op": "advance",
+ "now": 2000
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/min-burst.json b/packages/typing-fixtures/oracle/min-burst.json
new file mode 100644
index 000000000..8d79fa626
--- /dev/null
+++ b/packages/typing-fixtures/oracle/min-burst.json
@@ -0,0 +1,142 @@
+{
+ "name": "min-burst",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111, 110, 101, 32],
+ "active": false,
+ "ended": true,
+ "reason": "min burst"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10000,
+ "data": {
+ "inputType": "insertText",
+ "data": [110],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [111, 110]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10000,
+ "data": {
+ "inputType": "insertText",
+ "data": [101],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 2,
+ "inputValue": [111, 110, 101]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 10000,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 3,
+ "inputValue": [111, 110, 101, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 10000,
+ "data": {
+ "event": "end",
+ "timer": 10,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/time-stall-finish.json b/packages/typing-fixtures/oracle/time-stall-finish.json
new file mode 100644
index 000000000..8f97907c8
--- /dev/null
+++ b/packages/typing-fixtures/oracle/time-stall-finish.json
@@ -0,0 +1,212 @@
+{
+ "name": "time-stall-finish",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "3",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "end",
+ "timer": 5,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "3",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "end",
+ "timer": 5,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "3",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/time-zero.json b/packages/typing-fixtures/oracle/time-zero.json
new file mode 100644
index 000000000..9141c0474
--- /dev/null
+++ b/packages/typing-fixtures/oracle/time-zero.json
@@ -0,0 +1,239 @@
+{
+ "name": "time-zero",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 4000
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [111],
+ "active": false,
+ "ended": true
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [111],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [111]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 3,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 4,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 5000,
+ "data": {
+ "event": "step",
+ "timer": 5,
+ "drift": 4000
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 6000,
+ "data": {
+ "event": "end",
+ "timer": 6,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111, 32]
+ ],
+ "mode": "time",
+ "mode2": "0",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/timer-min-accuracy.json b/packages/typing-fixtures/oracle/timer-min-accuracy.json
new file mode 100644
index 000000000..7441cecc3
--- /dev/null
+++ b/packages/typing-fixtures/oracle/timer-min-accuracy.json
@@ -0,0 +1,114 @@
+{
+ "name": "timer-min-accuracy",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [120],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 0,
+ "inputValue": [120],
+ "active": false,
+ "ended": true,
+ "reason": "min accuracy"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [120],
+ "correct": false,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [120]
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "drift": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 1000,
+ "data": {
+ "event": "end",
+ "timer": 1,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [
+ [111, 110, 101, 32],
+ [116, 119, 111]
+ ],
+ "mode": "words",
+ "mode2": "2",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/typing-fixtures/oracle/timer-min-speed.json b/packages/typing-fixtures/oracle/timer-min-speed.json
new file mode 100644
index 000000000..b5ca14927
--- /dev/null
+++ b/packages/typing-fixtures/oracle/timer-min-speed.json
@@ -0,0 +1,293 @@
+{
+ "name": "timer-min-speed",
+ "frames": [
+ {
+ "state": {
+ "wordIndex": 4,
+ "inputValue": [],
+ "active": true,
+ "ended": false
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [98],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [98]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [98, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [99],
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": [99]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": [99, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [100],
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": [100]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": [100, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 32], [98, 32], [99, 32], [100, 32], [101]],
+ "mode": "words",
+ "mode2": "5",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ },
+ {
+ "state": {
+ "wordIndex": 4,
+ "inputValue": [],
+ "active": false,
+ "ended": true,
+ "reason": "min speed"
+ },
+ "log": {
+ "version": 1,
+ "events": [
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [97],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 0,
+ "inputValue": [97]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 0,
+ "charIndex": 1,
+ "inputValue": [97, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [98],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 0,
+ "inputValue": [98]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 1,
+ "charIndex": 1,
+ "inputValue": [98, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [99],
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 0,
+ "inputValue": [99]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 2,
+ "charIndex": 1,
+ "inputValue": [99, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [100],
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 0,
+ "inputValue": [100]
+ }
+ },
+ {
+ "type": "input",
+ "testMs": 0,
+ "data": {
+ "inputType": "insertText",
+ "data": [32],
+ "correct": true,
+ "wordIndex": 3,
+ "charIndex": 1,
+ "inputValue": [100, 32],
+ "commitsWord": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 0,
+ "data": {
+ "event": "start",
+ "timer": 0,
+ "date": 0
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2000,
+ "data": {
+ "event": "step",
+ "timer": 1,
+ "catchup": true
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2000,
+ "data": {
+ "event": "step",
+ "timer": 2,
+ "drift": 1000
+ }
+ },
+ {
+ "type": "timer",
+ "testMs": 2000,
+ "data": {
+ "event": "end",
+ "timer": 2,
+ "date": 0
+ }
+ }
+ ],
+ "context": {
+ "targetWords": [[97, 32], [98, 32], [99, 32], [100, 32], [101]],
+ "mode": "words",
+ "mode2": "5",
+ "koreanStatus": false,
+ "bailedOut": false,
+ "isFunboxWithNospacePropertyActive": false
+ }
+ }
+ }
+ ]
+}
From 9dce5932dd3e03cca285d499b9c62e3ec1f4f83d Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:14:06 +0530
Subject: [PATCH 13/93] fix(engine): preserve Korean scoring for every legacy
UTF-16 unit
---
crates/typing-engine/src/stats.rs | 17 ++++++-
crates/typing-engine/src/text.rs | 60 ++++++++++++++++++++++++
crates/typing-engine/tests/statistics.rs | 17 +++++++
docs/rust-engine/decisions.md | 3 ++
4 files changed, 95 insertions(+), 2 deletions(-)
diff --git a/crates/typing-engine/src/stats.rs b/crates/typing-engine/src/stats.rs
index eb13aa787..425ce7f94 100644
--- a/crates/typing-engine/src/stats.rs
+++ b/crates/typing-engine/src/stats.rs
@@ -143,10 +143,23 @@ pub fn chars(log: &Log, partial: bool, limit: Option) -> Chars {
*unit = 32;
}
}
- let target = log.context.target_words.get(index).unwrap_or(&typed);
+ let target = log
+ .context
+ .target_words
+ .get(index)
+ .unwrap_or(&typed)
+ .clone();
+ let (typed, target) = if log.context.korean_status {
+ (
+ crate::text::disassemble_hangul(&typed),
+ crate::text::disassemble_hangul(&target),
+ )
+ } else {
+ (typed, target)
+ };
let counts = count_chars(
&typed,
- target,
+ &target,
index == active && (partial || timed(log) || log.context.bailed_out),
);
total.all_correct += counts.all_correct;
diff --git a/crates/typing-engine/src/text.rs b/crates/typing-engine/src/text.rs
index f1daa7f2d..3f4c2ccd5 100644
--- a/crates/typing-engine/src/text.rs
+++ b/crates/typing-engine/src/text.rs
@@ -77,3 +77,63 @@ pub fn delete_word(input: &Text) -> Text {
}
Text(input.0[..end].to_vec())
}
+
+/// Match hangul-js disassemble: compatibility compound jamo split; modern jamo
+/// remain unchanged. Scores count these units while event indices stay UTF-16.
+pub fn disassemble_hangul(text: &Text) -> Text {
+ let leading: Vec<_> = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
+ .encode_utf16()
+ .collect();
+ let trailing: Vec<_> = "\0ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"
+ .encode_utf16()
+ .collect();
+ fn push(unit: u16, output: &mut Vec) {
+ // hangul-js initializes its lookup hashes with 0 -> 0; preserve that
+ // historical NUL interpretation rather than silently changing scores.
+ if unit == 0 {
+ output.push(0x3131);
+ return;
+ }
+ let pair = match unit {
+ 0x3133 => Some((0x3131, 0x3145)),
+ 0x3135 => Some((0x3134, 0x3148)),
+ 0x3136 => Some((0x3134, 0x314e)),
+ 0x313a => Some((0x3139, 0x3131)),
+ 0x313b => Some((0x3139, 0x3141)),
+ 0x313c => Some((0x3139, 0x3142)),
+ 0x313d => Some((0x3139, 0x3145)),
+ 0x313e => Some((0x3139, 0x314c)),
+ 0x313f => Some((0x3139, 0x314d)),
+ 0x3140 => Some((0x3139, 0x314e)),
+ 0x3144 => Some((0x3142, 0x3145)),
+ 0x3158 => Some((0x3157, 0x314f)),
+ 0x3159 => Some((0x3157, 0x3150)),
+ 0x315a => Some((0x3157, 0x3163)),
+ 0x315d => Some((0x315c, 0x3153)),
+ 0x315e => Some((0x315c, 0x3154)),
+ 0x315f => Some((0x315c, 0x3163)),
+ 0x3162 => Some((0x3161, 0x3163)),
+ _ => None,
+ };
+ if let Some((a, b)) = pair {
+ output.push(a);
+ output.push(b);
+ } else {
+ output.push(unit);
+ }
+ }
+ let mut result = vec![];
+ for unit in &text.0 {
+ if (0xac00..=0xd7a3).contains(unit) {
+ let syllable = usize::from(*unit - 0xac00);
+ push(leading[syllable / 588], &mut result);
+ push(0x314f + (syllable % 588 / 28) as u16, &mut result);
+ if syllable % 28 != 0 {
+ push(trailing[syllable % 28], &mut result);
+ }
+ } else {
+ push(*unit, &mut result);
+ }
+ }
+ Text(result)
+}
diff --git a/crates/typing-engine/tests/statistics.rs b/crates/typing-engine/tests/statistics.rs
index b53ae420e..1782afcf1 100644
--- a/crates/typing-engine/tests/statistics.rs
+++ b/crates/typing-engine/tests/statistics.rs
@@ -53,3 +53,20 @@ fn summary_fields_match_all_unchanged_browser_results_exactly() {
}
}
}
+
+#[test]
+fn hangul_disassembly_matches_frozen_original_library_for_every_utf16_unit() {
+ // Frozen from hangul-js 0.2.6, all 65536 individual UTF-16 code units.
+ // FNV-1a64 over each result's unit length followed by its UTF-16 units.
+ let mut hash = 14695981039346656037_u64;
+ let mut count = 0;
+ for unit in 0..=u16::MAX {
+ let result = typing_engine::text::disassemble_hangul(&Text(vec![unit]));
+ for unit in std::iter::once(result.len() as u16).chain(result.0) {
+ hash = (hash ^ u64::from(unit)).wrapping_mul(1099511628211);
+ count += 1;
+ }
+ }
+ assert_eq!(count, 161148);
+ assert_eq!(hash, 0x6b0ab93734a2fb3f);
+}
diff --git a/docs/rust-engine/decisions.md b/docs/rust-engine/decisions.md
index d20322e9e..1550a01be 100644
--- a/docs/rust-engine/decisions.md
+++ b/docs/rust-engine/decisions.md
@@ -11,3 +11,6 @@
before finalizing Phase 1.3; no lower MSRV support claimed without a build.
- Offline terminal initial scope excludes accounts/sync. Publication follows
repository release authorization; no production deployment implied by this task.
+- Korean scoring preserves hangul-js 0.2.6 disassembly, including its NUL→ㄱ
+ lookup quirk. An exhaustive frozen digest covers every UTF-16 unit. Correcting
+ that legacy interpretation requires a separately versioned behavior change.
From 4e1832bc9e399174ea30511d45c1865fd25946d5 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:14:08 +0530
Subject: [PATCH 14/93] test(engine): exercise every frozen command trace in
real browser Wasm
---
frontend/e2e/engine-proof.migration.ts | 19 ++++++++++++++++++-
knip.ts | 7 +++++++
packages/typing-engine/package.json | 5 +++++
packages/typing-engine/vite.config.ts | 9 +++++++++
4 files changed, 39 insertions(+), 1 deletion(-)
create mode 100644 packages/typing-engine/vite.config.ts
diff --git a/frontend/e2e/engine-proof.migration.ts b/frontend/e2e/engine-proof.migration.ts
index 086561677..d9171b97d 100644
--- a/frontend/e2e/engine-proof.migration.ts
+++ b/frontend/e2e/engine-proof.migration.ts
@@ -1,4 +1,4 @@
-import { readFile, writeFile } from "node:fs/promises";
+import { readFile, readdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { expect, test } from "playwright/test";
@@ -90,4 +90,21 @@ test("native fixture matches browser Wasm through built Cloudflare assets", asyn
expect(
JSON.parse(await page.getByTestId("engine-proof-result").innerText()),
).toEqual(expected);
+ for (const name of (await readdir(resolve(root, "commands"))).filter(
+ (entry) => entry.endsWith(".json"),
+ )) {
+ await page
+ .getByRole("textbox", { name: "Command fixture" })
+ .fill(await readFile(resolve(root, "commands", name), "utf8"));
+ await page.getByRole("button", { name: "Run fixture" }).click();
+ await expect(page.getByTestId("engine-proof-result")).not.toBeEmpty();
+ const frozen: unknown = JSON.parse(
+ await readFile(resolve(root, "oracle", name), "utf8"),
+ );
+ expect(
+ JSON.parse(await page.getByTestId("engine-proof-result").innerText()),
+ name,
+ ).toEqual(frozen);
+ await expect(page.getByTestId("engine-proof-error")).toBeEmpty();
+ }
});
diff --git a/knip.ts b/knip.ts
index 8a222ac07..bbb7824f9 100644
--- a/knip.ts
+++ b/knip.ts
@@ -6,6 +6,8 @@ import process from "node:process";
process.env.TURNSTILE_SITE_KEY ??= "1x00000000000000000000AA";
export default {
+ // Installed through the pinned Cargo bootstrap, not a JavaScript dependency.
+ ignoreBinaries: ["wasm-bindgen"],
workspaces: {
".": {
entry: [
@@ -65,6 +67,11 @@ export default {
"packages/typing-core": {
entry: ["scripts/*.ts"],
},
+ "packages/typing-engine": {
+ // This package's browser-only declarations use the workspace TS base;
+ // Knip's default entry analysis does not inspect this tsconfig reference.
+ ignoreDependencies: ["@oxytype/typescript-config"],
+ },
"packages/oxlint-config": {
entry: ["plugins/*.js"],
project: ["plugins/*.js"],
diff --git a/packages/typing-engine/package.json b/packages/typing-engine/package.json
index 8a7ef8160..f65f47c42 100644
--- a/packages/typing-engine/package.json
+++ b/packages/typing-engine/package.json
@@ -6,6 +6,11 @@
"exports": {
"./browser.client": "./src/browser.client.ts"
},
+ "scripts": {
+ "ts-check": "vp lint . --type-aware --type-check --format agent",
+ "lint": "vp lint . --type-aware --type-check --format agent",
+ "lint-fast": "vp lint . --format agent"
+ },
"devDependencies": {
"@oxytype/typescript-config": "workspace:*",
"vite-plus": "catalog:"
diff --git a/packages/typing-engine/vite.config.ts b/packages/typing-engine/vite.config.ts
new file mode 100644
index 000000000..50f4c8d73
--- /dev/null
+++ b/packages/typing-engine/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vite-plus";
+import { sharedLint } from "../oxlint-config/config";
+
+export default defineConfig({
+ lint: {
+ extends: [sharedLint],
+ ignorePatterns: ["generated", "node_modules"],
+ },
+});
From 0f497ab66cf3f1869421e73762d861d62a4d6974 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:24:40 +0530
Subject: [PATCH 15/93] feat(engine): port owned pace weak-spot learning and
typing-time state
---
crates/typing-engine/src/lib.rs | 2 +
crates/typing-engine/src/pace.rs | 151 ++++++++++++++++++++++++
crates/typing-engine/src/weak_spot.rs | 66 +++++++++++
crates/typing-engine/tests/pace.rs | 67 +++++++++++
crates/typing-engine/tests/weak_spot.rs | 16 +++
5 files changed, 302 insertions(+)
create mode 100644 crates/typing-engine/src/pace.rs
create mode 100644 crates/typing-engine/src/weak_spot.rs
create mode 100644 crates/typing-engine/tests/pace.rs
create mode 100644 crates/typing-engine/tests/weak_spot.rs
diff --git a/crates/typing-engine/src/lib.rs b/crates/typing-engine/src/lib.rs
index 185ab9fc1..a90c28b7c 100644
--- a/crates/typing-engine/src/lib.rs
+++ b/crates/typing-engine/src/lib.rs
@@ -1,7 +1,9 @@
//! Platform-independent typing rules. Hosts provide content, observations and time.
+pub mod pace;
pub mod session;
pub mod stats;
pub mod text;
+pub mod weak_spot;
pub use session::{Command, Config, Session};
pub use text::Text;
diff --git a/crates/typing-engine/src/pace.rs b/crates/typing-engine/src/pace.rs
new file mode 100644
index 000000000..be77a8074
--- /dev/null
+++ b/crates/typing-engine/src/pace.rs
@@ -0,0 +1,151 @@
+use crate::Text;
+use serde::Serialize;
+use std::collections::BTreeSet;
+use ts_rs::TS;
+
+#[derive(Clone, Debug, Serialize, TS, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct Position {
+ pub word_index: usize,
+ pub letter_index: usize,
+ pub wpm: f64,
+}
+#[derive(Default)]
+pub struct Pace {
+ wpm: Option,
+ word_index: usize,
+ letter_index: usize,
+ correction: isize,
+ mistakes: BTreeSet,
+ start_ms: f64,
+ steps: u64,
+ last_wpm: f64,
+}
+impl Pace {
+ pub fn reset(&mut self, wpm: Option) -> bool {
+ self.wpm = wpm.filter(|value| value.is_finite() && *value >= 1.0);
+ self.word_index = 0;
+ self.letter_index = 0;
+ self.correction = 0;
+ self.mistakes.clear();
+ self.start_ms = 0.0;
+ self.steps = 0;
+ self.wpm.is_some()
+ }
+ pub fn start(&mut self, now: f64) {
+ self.start_ms = now;
+ self.steps = 0;
+ }
+ pub fn position(&self) -> Option {
+ self.wpm.map(|wpm| Position {
+ word_index: self.word_index,
+ letter_index: self.letter_index,
+ wpm,
+ })
+ }
+ pub fn correct(&mut self, index: usize, correct: bool, word: &Text, blind: bool) {
+ if self.wpm.is_none() || blind {
+ return;
+ }
+ if correct {
+ if self.mistakes.remove(&index) {
+ self.correction -= word.len() as isize;
+ }
+ } else if self.mistakes.insert(index) {
+ self.correction += word.len() as isize;
+ }
+ }
+ pub fn step(&mut self, words: &[Text], blind: bool) -> Option {
+ self.wpm?;
+ let Some(word) = words.get(self.word_index) else {
+ self.wpm = None;
+ return None;
+ };
+ if self.letter_index >= word.len() {
+ self.letter_index = 0;
+ self.word_index += 1;
+ } else {
+ self.letter_index += 1;
+ }
+ if !blind {
+ while self.correction < 0 {
+ if self.letter_index == 0 {
+ let Some(previous) = self
+ .word_index
+ .checked_sub(1)
+ .and_then(|index| words.get(index))
+ else {
+ self.wpm = None;
+ return None;
+ };
+ self.word_index -= 1;
+ self.letter_index = previous.len();
+ } else {
+ self.letter_index -= 1;
+ }
+ self.correction += 1;
+ }
+ while self.correction > 0 {
+ let Some(word) = words.get(self.word_index) else {
+ self.wpm = None;
+ return None;
+ };
+ self.letter_index += 1;
+ if self.letter_index > word.len() {
+ self.letter_index = 0;
+ self.word_index += 1;
+ }
+ self.correction -= 1;
+ }
+ }
+ self.steps += 1;
+ self.position()
+ }
+ pub fn advance(&mut self, now: f64, words: &[Text], blind: bool) -> Option {
+ let speed = self.wpm?;
+ let due = ((now - self.start_ms) / (60.0 / (speed * 5.0) * 1000.0)).floor();
+ while (self.steps as f64) < due {
+ self.step(words, blind)?;
+ }
+ self.position()
+ }
+ pub fn step_end(&self) -> f64 {
+ self.start_ms
+ + self.steps as f64 * self.wpm.map(|wpm| 60.0 / (wpm * 5.0)).unwrap_or(0.0) * 1000.0
+ }
+ pub fn remember(&mut self, wpm: f64, repeating: bool) {
+ if !repeating || wpm > self.last_wpm {
+ self.last_wpm = wpm;
+ }
+ }
+ pub fn last_wpm(&self) -> f64 {
+ self.last_wpm
+ }
+}
+
+#[derive(Default)]
+pub struct TypingTime {
+ seconds: f64,
+ day: Option,
+}
+impl TypingTime {
+ /// The host supplies its local-calendar day token; no hidden system clock.
+ pub fn add(&mut self, seconds: f64, today: i64) {
+ if self.day.is_some_and(|day| day != today) {
+ self.day = Some(today);
+ self.seconds = seconds;
+ } else {
+ self.seconds += seconds;
+ }
+ }
+ pub fn load_today(&mut self, seconds: f64, today: i64) {
+ self.day = Some(today);
+ self.seconds = seconds;
+ }
+ pub fn seconds(&self) -> f64 {
+ self.seconds
+ }
+ pub fn is_today(&self) -> bool {
+ self.day.is_some()
+ }
+}
diff --git a/crates/typing-engine/src/weak_spot.rs b/crates/typing-engine/src/weak_spot.rs
new file mode 100644
index 000000000..bcb2b8a05
--- /dev/null
+++ b/crates/typing-engine/src/weak_spot.rs
@@ -0,0 +1,66 @@
+use crate::Text;
+use std::collections::BTreeMap;
+
+#[derive(Default)]
+pub struct WeakSpot {
+ scores: BTreeMap, Score>,
+}
+#[derive(Default)]
+struct Score {
+ average: f64,
+ count: u32,
+}
+impl WeakSpot {
+ pub fn update(&mut self, text: &Text, correct: bool, spacing: Option) {
+ let Some(spacing) = spacing else {
+ return;
+ };
+ let score = self.scores.entry(text.0.clone()).or_default();
+ score.count = (score.count + 1).min(50);
+ let rate = 1.0 / f64::from(score.count);
+ let value = spacing + if correct { 0.0 } else { 5000.0 };
+ score.average = value * rate + score.average * (1.0 - rate);
+ }
+ fn score(&self, word: &Text) -> f64 {
+ let mut total = 0.0;
+ let mut count = 0;
+ let mut index = 0;
+ // Match JS for-of code-point iteration when scoring candidate words.
+ while index < word.len() {
+ let width = if (0xd800..=0xdbff).contains(&word.0[index])
+ && word
+ .0
+ .get(index + 1)
+ .is_some_and(|c| (0xdc00..=0xdfff).contains(c))
+ {
+ 2
+ } else {
+ 1
+ };
+ if let Some(score) = self.scores.get(&word.0[index..index + width]) {
+ total += score.average;
+ count += 1;
+ }
+ index += width;
+ }
+ if count == 0 {
+ 0.0
+ } else {
+ total / f64::from(count)
+ }
+ }
+ /// Generator supplies the same twenty RNG-selected candidates; ties retain
+ /// the earliest sample, exactly as the browser learner does.
+ pub fn best<'a>(&self, candidates: &'a [Text]) -> Option<&'a Text> {
+ let mut best = None;
+ let mut high = 0.0;
+ for candidate in candidates {
+ let score = self.score(candidate);
+ if best.is_none() || score > high {
+ best = Some(candidate);
+ high = score;
+ }
+ }
+ best
+ }
+}
diff --git a/crates/typing-engine/tests/pace.rs b/crates/typing-engine/tests/pace.rs
new file mode 100644
index 000000000..feeaf4a14
--- /dev/null
+++ b/crates/typing-engine/tests/pace.rs
@@ -0,0 +1,67 @@
+use typing_engine::{
+ Text,
+ pace::{Pace, Position, TypingTime},
+};
+#[test]
+fn pace_corrects_each_error_once_and_remembers_repeated_speed() {
+ let words = vec![Text::from("abc"), Text::from("def"), Text::from("ghi")];
+ let mut pace = Pace::default();
+ assert!(pace.reset(Some(120.0)));
+ pace.correct(0, false, &Text::from("abc "), false);
+ pace.correct(0, false, &Text::from("abc "), false);
+ assert_eq!(
+ pace.step(&words, false),
+ Some(Position {
+ word_index: 1,
+ letter_index: 1,
+ wpm: 120.0
+ })
+ );
+ pace.correct(0, true, &Text::from("abc "), false);
+ assert_eq!(
+ pace.step(&words, false),
+ Some(Position {
+ word_index: 0,
+ letter_index: 2,
+ wpm: 120.0
+ })
+ );
+ pace.remember(70.0, false);
+ pace.remember(40.0, true);
+ assert_eq!(pace.last_wpm(), 70.0);
+ pace.remember(90.0, true);
+ assert!(!pace.reset(None));
+ assert_eq!(pace.last_wpm(), 90.0);
+}
+#[test]
+fn pace_catches_up_from_start_then_disables_when_out_of_words() {
+ let words = vec![Text::from("hi"), Text::from("you"), Text::from("all")];
+ let mut pace = Pace::default();
+ pace.reset(Some(60.0));
+ pace.start(0.0);
+ assert_eq!(pace.advance(199.0, &words, false).unwrap().letter_index, 0);
+ assert_eq!(
+ pace.advance(1200.0, &words, false),
+ Some(Position {
+ word_index: 1,
+ letter_index: 3,
+ wpm: 60.0
+ })
+ );
+ assert!(pace.advance(5000.0, &words, false).is_none());
+ assert!(!pace.reset(Some(f64::INFINITY)));
+}
+#[test]
+fn typing_time_uses_explicit_local_day_and_preserves_session_before_load() {
+ let mut time = TypingTime::default();
+ time.add(10.0, 1);
+ time.add(10.0, 2);
+ assert_eq!(time.seconds(), 20.0);
+ assert!(!time.is_today());
+ time.load_today(10.0, 2);
+ time.add(5.0, 2);
+ assert_eq!(time.seconds(), 15.0);
+ time.add(10.0, 3);
+ time.add(5.0, 3);
+ assert_eq!(time.seconds(), 15.0);
+}
diff --git a/crates/typing-engine/tests/weak_spot.rs b/crates/typing-engine/tests/weak_spot.rs
new file mode 100644
index 000000000..0c5b946a6
--- /dev/null
+++ b/crates/typing-engine/tests/weak_spot.rs
@@ -0,0 +1,16 @@
+use typing_engine::{Text, weak_spot::WeakSpot};
+#[test]
+fn learner_penalizes_errors_and_remains_isolated() {
+ let candidates = vec![Text::from("a"), Text::from("x")];
+ let mut learner = WeakSpot::default();
+ learner.update(&Text::from("x"), false, Some(100.0));
+ assert_eq!(learner.best(&candidates), Some(&candidates[1]));
+ assert_eq!(WeakSpot::default().best(&candidates), Some(&candidates[0]));
+}
+#[test]
+fn unknown_spacing_is_not_learned_as_measured_zero() {
+ let candidates = vec![Text::from("a"), Text::from("x")];
+ let mut learner = WeakSpot::default();
+ learner.update(&Text::from("x"), false, None);
+ assert_eq!(learner.best(&candidates), Some(&candidates[0]));
+}
From e95c2957c5bf8c3fc9bc7d07920fcad4e1727902 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:24:45 +0530
Subject: [PATCH 16/93] feat(engine): match frozen Mulberry32 streams and
explicit RNG exhaustion
---
crates/typing-engine/src/lib.rs | 1 +
crates/typing-engine/src/random.rs | 64 ++++++++++++++++++
crates/typing-engine/tests/random.rs | 28 ++++++++
.../oracle/random-streams.json | 67 +++++++++++++++++++
4 files changed, 160 insertions(+)
create mode 100644 crates/typing-engine/src/random.rs
create mode 100644 crates/typing-engine/tests/random.rs
create mode 100644 packages/typing-fixtures/oracle/random-streams.json
diff --git a/crates/typing-engine/src/lib.rs b/crates/typing-engine/src/lib.rs
index a90c28b7c..34375a90b 100644
--- a/crates/typing-engine/src/lib.rs
+++ b/crates/typing-engine/src/lib.rs
@@ -1,5 +1,6 @@
//! Platform-independent typing rules. Hosts provide content, observations and time.
pub mod pace;
+pub mod random;
pub mod session;
pub mod stats;
pub mod text;
diff --git a/crates/typing-engine/src/random.rs b/crates/typing-engine/src/random.rs
new file mode 100644
index 000000000..20d5d90cd
--- /dev/null
+++ b/crates/typing-engine/src/random.rs
@@ -0,0 +1,64 @@
+use serde::{Deserialize, Serialize};
+
+/// Stable Mulberry32 stream, matching the original generator fixtures. No host
+/// RNG, hidden clock or library-selected algorithm participates in generation.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(tag = "kind", rename_all = "camelCase")]
+pub enum Random {
+ Seeded { state: u32 },
+ Stream { values: Vec, cursor: usize },
+}
+impl Random {
+ pub fn seeded(seed: u32) -> Self {
+ Self::Seeded { state: seed }
+ }
+ pub fn stream(values: Vec) -> Result {
+ if values
+ .iter()
+ .any(|value| !value.is_finite() || !(0.0..1.0).contains(value))
+ {
+ return Err("Random stream values must be finite and in [0,1)".into());
+ }
+ Ok(Self::Stream { values, cursor: 0 })
+ }
+ pub fn draw(&mut self) -> Result {
+ match self {
+ Self::Seeded { state } => {
+ *state = state.wrapping_add(0x6d2b79f5);
+ let mut t = (*state ^ (*state >> 15)).wrapping_mul(1 | *state);
+ t = t.wrapping_add((t ^ (t >> 7)).wrapping_mul(61 | t)) ^ t;
+ Ok(f64::from(t ^ (t >> 14)) / 4294967296.0)
+ }
+ Self::Stream { values, cursor } => {
+ let value = *values.get(*cursor).ok_or("Random stream exhausted")?;
+ *cursor += 1;
+ Ok(value)
+ }
+ }
+ }
+ pub fn index(&mut self, length: usize) -> Result {
+ if length == 0 {
+ return Err("Cannot choose from empty content".into());
+ }
+ Ok((self.draw()? * length as f64).floor() as usize)
+ }
+ pub fn zipf_index(&mut self, length: usize) -> Result {
+ if length == 0 {
+ return Err("Cannot choose from empty content".into());
+ }
+ let gamma = std::f64::consts::EULER_GAMMA;
+ let harmonic = (length as f64 + 0.5).ln() + gamma;
+ let index = ((self.draw()? * harmonic - gamma).exp() - 0.5).floor();
+ if index < 0.0 || index >= length as f64 {
+ return Err("Zipf draw outside wordset".into());
+ }
+ Ok(index as usize)
+ }
+ pub fn shuffle(&mut self, values: &mut [T]) -> Result<(), String> {
+ for index in (1..values.len()).rev() {
+ let other = self.index(index + 1)?;
+ values.swap(index, other);
+ }
+ Ok(())
+ }
+}
diff --git a/crates/typing-engine/tests/random.rs b/crates/typing-engine/tests/random.rs
new file mode 100644
index 000000000..a9337af4e
--- /dev/null
+++ b/crates/typing-engine/tests/random.rs
@@ -0,0 +1,28 @@
+use typing_engine::random::Random;
+#[test]
+fn seeded_draw_order_matches_frozen_javascript_mulberry32() {
+ let fixtures: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../packages/typing-fixtures/oracle/random-streams.json"
+ ))
+ .unwrap();
+ for (seed, values) in fixtures.as_object().unwrap() {
+ let mut rng = Random::seeded(seed.parse().unwrap());
+ for expected in values.as_array().unwrap() {
+ assert_eq!(
+ rng.draw().unwrap(),
+ expected.as_f64().unwrap(),
+ "seed {seed}"
+ );
+ }
+ }
+}
+#[test]
+fn explicit_streams_reject_invalid_values_and_never_reseed_on_exhaustion() {
+ assert!(Random::stream(vec![f64::NAN]).is_err());
+ assert!(Random::stream(vec![1.0]).is_err());
+ let mut rng = Random::stream(vec![0.0, 0.5, 0.999]).unwrap();
+ assert_eq!(rng.index(4).unwrap(), 0);
+ assert_eq!(rng.index(4).unwrap(), 2);
+ assert_eq!(rng.index(4).unwrap(), 3);
+ assert!(rng.draw().is_err());
+}
diff --git a/packages/typing-fixtures/oracle/random-streams.json b/packages/typing-fixtures/oracle/random-streams.json
new file mode 100644
index 000000000..d9c447b87
--- /dev/null
+++ b/packages/typing-fixtures/oracle/random-streams.json
@@ -0,0 +1,67 @@
+{
+ "0": [
+ 0.26642920868471265, 0.0003297457005828619, 0.2232720274478197,
+ 0.1462021479383111, 0.46732782293111086, 0.5450490827206522,
+ 0.6152513844426721, 0.6489853798411787, 0.45600721263326705,
+ 0.581218967679888, 0.24625753005966544, 0.09785169479437172,
+ 0.9034309543203562, 0.40820372267626226, 0.7202955442480743,
+ 0.6599067240022123, 0.8058585098478943, 0.6261914535425603,
+ 0.5696913106366992, 0.12123633874580264, 0.24193100933916867,
+ 0.8153759187553078, 0.323521607555449, 0.08783002896234393,
+ 0.7895766077563167, 0.9079886567778885, 0.551719788229093,
+ 0.4687971824314445, 0.3561653404030949, 0.09744705795310438,
+ 0.752272559562698, 0.52662223437801
+ ],
+ "1": [
+ 0.6270739405881613, 0.002735721180215478, 0.5274470399599522,
+ 0.9810509674716741, 0.9683778982143849, 0.281103502959013,
+ 0.6128388606011868, 0.7207431411370635, 0.425796952098608,
+ 0.9948229456786066, 0.45527112716808915, 0.48878831742331386,
+ 0.13893358339555562, 0.40374476509168744, 0.24752389639616013,
+ 0.15449313493445516, 0.48914389545097947, 0.0673698834143579,
+ 0.3946193160954863, 0.7671080259606242, 0.28586167353205383,
+ 0.19087357842363417, 0.042729709995910525, 0.42738195578567684,
+ 0.590382493333891, 0.7986842156387866, 0.29486987087875605,
+ 0.2054697866551578, 0.6581296445801854, 0.5460823616012931,
+ 0.758442840538919, 0.19281403115019202
+ ],
+ "42": [
+ 0.6011037519201636, 0.44829055899754167, 0.8524657934904099,
+ 0.6697340414393693, 0.17481389874592423, 0.5265925421845168,
+ 0.2732279943302274, 0.6247446539346129, 0.8654746483080089,
+ 0.4723170551005751, 0.24992373422719538, 0.8820588334929198,
+ 0.7457375649828464, 0.3070015134289861, 0.19725383794866502,
+ 0.5007294877432287, 0.6866120179183781, 0.6106208984274417,
+ 0.003842951962724328, 0.47078192373737693, 0.8373374259099364,
+ 0.05120926629751921, 0.5923239905387163, 0.03153795562684536,
+ 0.2669559868518263, 0.06178139243274927, 0.18568900716491044,
+ 0.7835472931619734, 0.530335606308654, 0.027123609324917197,
+ 0.17300523445010185, 0.8426881253253669
+ ],
+ "2147483648": [
+ 0.8205775609239936, 0.4481089550536126, 0.7836112855002284,
+ 0.5120457962621003, 0.8388098266441375, 0.4205148529727012,
+ 0.8944806265644729, 0.9326926763169467, 0.6747888037934899,
+ 0.4173516221344471, 0.7520525439176708, 0.5010397543665022,
+ 0.3403281669598073, 0.9581121851224452, 0.13500668411143124,
+ 0.6329791664611548, 0.4525297924410552, 0.9778232092503458,
+ 0.8422936180140823, 0.22736566024832428, 0.2501462288200855,
+ 0.5457336774561554, 0.13329570461064577, 0.9689932239707559,
+ 0.7632795765530318, 0.049264932284131646, 0.8356091578025371,
+ 0.498972142348066, 0.7754034039098769, 0.7437046824488789,
+ 0.6968864763621241, 0.5806966486852616
+ ],
+ "4294967295": [
+ 0.8964226141106337, 0.189478256739676, 0.7156526781618595,
+ 0.9440599093213677, 0.8452364315744489, 0.5391399988438934,
+ 0.6804977387655526, 0.4755720964167267, 0.13585773925296962,
+ 0.9884445976931602, 0.8714674517977983, 0.18934845877811313,
+ 0.3436866346746683, 0.7386848733294755, 0.4759919897187501,
+ 0.7567116804420948, 0.9159235202241689, 0.10207696096040308,
+ 0.1436101261060685, 0.3921705954708159, 0.9202409950084984,
+ 0.9238722093869001, 0.7916392679326236, 0.8520552832633257,
+ 0.1153229579795152, 0.09125144220888615, 0.5151041666977108,
+ 0.2751815707888454, 0.3076603573281318, 0.01972890179604292,
+ 0.4548310365062207, 0.2538063288666308
+ ]
+}
From 38e98e9487024c6fdee2c7cbe677448fdcd563bf Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:24:49 +0530
Subject: [PATCH 17/93] feat(engine): preserve browser commit composition and
deferred input lifecycle
---
crates/typing-engine/src/session.rs | 126 ++++++++++++++++--
crates/typing-engine/tests/observations.rs | 81 +++++++++++
crates/typing-engine/tests/recordings.rs | 25 ++++
.../e2e/engine-input-contract.migration.ts | 34 +++++
4 files changed, 258 insertions(+), 8 deletions(-)
create mode 100644 crates/typing-engine/tests/observations.rs
create mode 100644 frontend/e2e/engine-input-contract.migration.ts
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
index 7449ba42a..e12d4059a 100644
--- a/crates/typing-engine/src/session.rs
+++ b/crates/typing-engine/src/session.rs
@@ -4,7 +4,7 @@ use crate::{
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, VecDeque};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -121,6 +121,18 @@ pub enum Command {
Finalize {
now: f64,
},
+ #[serde(rename_all = "camelCase")]
+ View {
+ now: f64,
+ #[serde(default)]
+ earliest_visible_word: Option,
+ #[serde(default)]
+ defer_automatic: Option,
+ },
+ FlushAutomatic {
+ now: f64,
+ generation: u32,
+ },
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
@@ -148,6 +160,9 @@ pub struct Event {
pub struct Update {
pub state: State,
pub events: Vec,
+ pub generation: u32,
+ #[serde(rename = "automaticPending")]
+ pub automatic_pending: bool,
}
#[derive(Clone)]
@@ -158,6 +173,8 @@ struct Recorded {
}
pub struct Session {
+ pub pace: crate::pace::Pace,
+ pub weak_spot: crate::weak_spot::WeakSpot,
config: Config,
words: Vec,
input: Vec,
@@ -171,6 +188,12 @@ pub struct Session {
indent_pending: bool,
pressed: Vec<(String, f64)>,
no_code_index: usize,
+ completion_checkpoint: bool,
+ composing: bool,
+ earliest_visible_word: usize,
+ defer_automatic: bool,
+ automatic: VecDeque<(f64, f64)>,
+ generation: u32,
}
impl Session {
@@ -182,6 +205,8 @@ impl Session {
return Err("Invalid time limit".into());
}
Ok(Self {
+ pace: crate::pace::Pace::default(),
+ weak_spot: crate::weak_spot::WeakSpot::default(),
config,
input: vec![Text::default(); words.len().max(1)],
words,
@@ -195,6 +220,12 @@ impl Session {
indent_pending: false,
pressed: vec![],
no_code_index: 0,
+ completion_checkpoint: true,
+ composing: false,
+ earliest_visible_word: 0,
+ defer_automatic: false,
+ automatic: VecDeque::new(),
+ generation: 0,
})
}
@@ -231,6 +262,7 @@ impl Session {
}
self.active = true;
self.start = Some(now);
+ self.pace.start(now);
self.record(
"timer",
now,
@@ -258,7 +290,9 @@ impl Session {
| Command::Abandon { now, .. }
| Command::Reset { now }
| Command::Observe { now, .. }
- | Command::Finalize { now } => *now,
+ | Command::Finalize { now }
+ | Command::View { now, .. }
+ | Command::FlushAutomatic { now, .. } => *now,
};
if !now.is_finite() || now.abs() > 9_007_199_254_740_991.0 {
return Err("Invalid timestamp".into());
@@ -273,10 +307,17 @@ impl Session {
automatic,
is_composition_ending,
} => {
- for unit in data.0 {
+ if is_composition_ending == Some(true) {
+ self.composing = false;
+ }
+ let length = data.len();
+ for (index, unit) in data.0.into_iter().enumerate() {
if self.ended {
break;
}
+ // The browser checks failure/finish at the final unit of a
+ // committed input batch, not after each surrogate/character.
+ self.completion_checkpoint = index + 1 == length && !self.composing;
self.insert(
unit,
now,
@@ -286,6 +327,7 @@ impl Session {
is_composition_ending == Some(true),
);
}
+ self.completion_checkpoint = true;
}
Command::Delete { kind, now, .. } => {
if !["deleteContentBackward", "deleteWordBackward"].contains(&kind.as_str()) {
@@ -311,6 +353,11 @@ impl Session {
self.input = vec![Text::default(); self.words.len().max(1)];
self.pressed.clear();
self.no_code_index = 0;
+ self.pace.reset(None);
+ self.composing = false;
+ self.earliest_visible_word = 0;
+ self.automatic.clear();
+ self.generation = self.generation.wrapping_add(1);
}
Command::Observe {
now,
@@ -318,6 +365,26 @@ impl Session {
event_data,
} => self.observe(&event_type, now, event_data)?,
Command::Finalize { .. } => self.finalize(),
+ Command::View {
+ earliest_visible_word,
+ defer_automatic,
+ ..
+ } => {
+ self.earliest_visible_word = earliest_visible_word.unwrap_or(0);
+ if let Some(defer) = defer_automatic {
+ self.defer_automatic = defer;
+ }
+ }
+ Command::FlushAutomatic { generation, .. } => {
+ if generation == self.generation
+ && !self.ended
+ && let Some((now, date)) = self.automatic.pop_front()
+ {
+ self.completion_checkpoint = !self.composing;
+ self.insert(9, now, date, None, true, false);
+ self.completion_checkpoint = true;
+ }
+ }
}
let origin = self.origin();
let events = self
@@ -330,6 +397,8 @@ impl Session {
Ok(Update {
state: self.state(),
events,
+ generation: self.generation,
+ automatic_pending: !self.automatic.is_empty(),
})
}
@@ -343,9 +412,19 @@ impl Session {
composition: bool,
) {
self.insert_one(unit, now, date, shift, automatic, composition);
+ if self.defer_automatic && self.indent_pending {
+ self.indent_pending = false;
+ self.automatic.push_back((now, date));
+ return;
+ }
while self.indent_pending && !self.ended {
self.indent_pending = false;
self.insert_one(9, now, date, None, true, false);
+ if self.defer_automatic && self.indent_pending {
+ self.indent_pending = false;
+ self.automatic.push_back((now, date));
+ break;
+ }
}
}
@@ -444,12 +523,28 @@ impl Session {
}
self.input
.resize(self.input.len().max(self.index + 1), Text::default());
+ if advance {
+ self.pace
+ .correct(self.index, next == target, &target, self.config.blind_mode);
+ }
self.input[self.index] = next;
+ let previous = self
+ .events
+ .iter()
+ .rev()
+ .find(|event| event.kind == "input")
+ .map(|event| event.now);
self.record("input", now, data);
+ self.weak_spot.update(
+ &Text(vec![unit]),
+ correct,
+ previous.map(|previous| round2(round2(now) - previous)),
+ );
if !correct && !stopped && self.config.delete_on_error != "off" {
self.delete_error(now);
}
- if self.config.mode != "zen"
+ if self.completion_checkpoint
+ && self.config.mode != "zen"
&& ((self.config.difficulty == "master" && !correct)
|| (self.config.difficulty == "expert"
&& commit
@@ -459,7 +554,7 @@ impl Session {
self.end(now, date, Some("difficulty".into()));
return;
}
- if advance && self.config.min_burst != "off" {
+ if self.completion_checkpoint && advance && self.config.min_burst != "off" {
let log: crate::stats::Log =
serde_json::from_value(self.log()).expect("Engine log is valid");
let burst = crate::stats::word_burst(&log, self.index, now - self.start.unwrap_or(now));
@@ -490,7 +585,11 @@ impl Session {
&& target.len() == input.len() + 1
&& self.config.stop_on_error == "off"
&& self.config.delete_on_error == "off";
- if self.index + 1 >= self.words.len() && finite && (word_correct || quick || advance) {
+ if self.completion_checkpoint
+ && self.index + 1 >= self.words.len()
+ && finite
+ && (word_correct || quick || advance)
+ {
self.end(now, date, None);
} else if advance && (self.index + 1 < self.words.len() || self.config.mode == "zen") {
self.index += 1;
@@ -551,7 +650,10 @@ impl Session {
);
}
}
- if self.config.delete_on_error.contains("hard") && input.len() <= 1 && self.index > 0 {
+ if self.config.delete_on_error.contains("hard")
+ && input.len() <= 1
+ && self.index > self.earliest_visible_word
+ {
self.index -= 1;
let previous = self.previous_input(kind);
self.log_delete(kind, now, previous.len(), previous, true);
@@ -563,7 +665,7 @@ impl Session {
return;
}
let input = self.current();
- if input.is_empty() && self.index == 0 {
+ if input.is_empty() && self.index <= self.earliest_visible_word {
return;
}
let prev = self
@@ -775,6 +877,14 @@ impl Session {
data["code"] = json!(key);
self.record(kind, now, data);
} else if kind == "composition" || kind == "timer" {
+ if kind == "composition" {
+ if data["event"] == "start" {
+ self.composing = true;
+ } else if data["event"] == "end" {
+ self.composing = false;
+ }
+ data["wordIndex"] = json!(self.index);
+ }
self.record(kind, now, data);
} else {
return Err("Unsupported observation type".into());
diff --git a/crates/typing-engine/tests/observations.rs b/crates/typing-engine/tests/observations.rs
new file mode 100644
index 000000000..a618abb05
--- /dev/null
+++ b/crates/typing-engine/tests/observations.rs
@@ -0,0 +1,81 @@
+use serde_json::json;
+use typing_engine::{Session, Text};
+fn apply(session: &mut Session, value: serde_json::Value) -> typing_engine::session::Update {
+ session
+ .apply(serde_json::from_value(value).unwrap())
+ .unwrap()
+}
+#[test]
+fn visible_word_bound_blocks_backtracking_even_with_freedom() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":3,"freedomMode":true})).unwrap(),
+ vec![Text::from("a "), Text::from("b "), Text::from("c")],
+ )
+ .unwrap();
+ apply(&mut session, json!({"op":"insert","data":[97,32],"now":0}));
+ apply(
+ &mut session,
+ json!({"op":"view","earliestVisibleWord":1,"now":10}),
+ );
+ apply(
+ &mut session,
+ json!({"op":"delete","kind":"deleteContentBackward","now":20}),
+ );
+ assert_eq!(session.state().word_index, 1);
+ assert!(session.state().input_value.is_empty());
+}
+#[test]
+fn deferred_indentation_retains_capture_time_and_reset_invalidates_old_tasks() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":2,"language":"code_javascript"}))
+ .unwrap(),
+ vec![Text::from("a\n"), Text::from("\t\tb")],
+ )
+ .unwrap();
+ apply(
+ &mut session,
+ json!({"op":"view","deferAutomatic":true,"now":0}),
+ );
+ let update = apply(
+ &mut session,
+ json!({"op":"insert","data":[97,10],"now":100}),
+ );
+ assert!(update.automatic_pending);
+ assert!(session.state().input_value.is_empty());
+ let next = apply(
+ &mut session,
+ json!({"op":"flushAutomatic","generation":update.generation,"now":200}),
+ );
+ assert_eq!(session.state().input_value, Text::from("\t"));
+ assert!(next.automatic_pending);
+ assert!(next.events.iter().all(|event| event.test_ms == 0.0));
+ apply(&mut session, json!({"op":"reset","now":250}));
+ let stale = apply(
+ &mut session,
+ json!({"op":"flushAutomatic","generation":update.generation,"now":300}),
+ );
+ assert!(stale.events.is_empty());
+ assert!(!stale.automatic_pending);
+ assert!(!session.state().active);
+}
+#[test]
+fn composition_defers_difficulty_until_the_committed_end() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":1,"difficulty":"master"})).unwrap(),
+ vec![Text::from("abc")],
+ )
+ .unwrap();
+ apply(
+ &mut session,
+ json!({"op":"observe","eventType":"composition","eventData":{"event":"start","wordIndex":99},"now":0}),
+ );
+ apply(&mut session, json!({"op":"insert","data":[120],"now":10}));
+ assert!(!session.state().ended);
+ apply(
+ &mut session,
+ json!({"op":"insert","data":[121],"isCompositionEnding":true,"now":20}),
+ );
+ assert!(session.state().ended);
+ assert_eq!(session.state().reason.as_deref(), Some("difficulty"));
+ assert_eq!(session.log()["events"][0]["data"]["wordIndex"], 0);
+}
diff --git a/crates/typing-engine/tests/recordings.rs b/crates/typing-engine/tests/recordings.rs
index 5d83761cc..004914ec3 100644
--- a/crates/typing-engine/tests/recordings.rs
+++ b/crates/typing-engine/tests/recordings.rs
@@ -127,3 +127,28 @@ fn absent_physical_observations_do_not_create_key_durations() {
.all(|event| event["type"] != "keyup" && event["type"] != "keydown")
);
}
+
+#[test]
+fn master_failure_checks_final_unit_as_recorded_by_actual_browser_handler() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":2,"difficulty":"master"})).unwrap(),
+ vec![Text::from("one "), Text::from("two")],
+ )
+ .unwrap();
+ session
+ .apply(
+ serde_json::from_value(json!({"op":"insert","data":[120,111,110,101,32],"now":100}))
+ .unwrap(),
+ )
+ .unwrap();
+ let log = session.log();
+ let inputs: Vec<_> = log["events"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .filter(|e| e["type"] == "input")
+ .collect();
+ assert_eq!(inputs.len(), 5);
+ assert_eq!(session.state().reason.as_deref(), Some("difficulty"));
+ assert!(session.state().ended);
+}
diff --git a/frontend/e2e/engine-input-contract.migration.ts b/frontend/e2e/engine-input-contract.migration.ts
new file mode 100644
index 000000000..3290007e5
--- /dev/null
+++ b/frontend/e2e/engine-input-contract.migration.ts
@@ -0,0 +1,34 @@
+import type { EventLog } from "@oxytype/typing-core/events/types";
+import { expect, openTest, test } from "./fixtures";
+
+test.use({
+ config: {
+ mode: "words",
+ words: 15,
+ difficulty: "master",
+ smoothCaret: "off",
+ },
+});
+
+test("master failure waits for the final unit of a multi-character browser commit", async ({
+ page,
+}) => {
+ await openTest(page);
+ const first = await page.locator("#words .word").first().textContent();
+ if (first === null) throw new Error("Missing target");
+ await page.locator("#words").click();
+ const batch = `x${first} `;
+ await page.keyboard.insertText(batch);
+ await expect(page.locator("#result")).toBeVisible();
+ const log = await page.evaluate(() =>
+ (window as unknown as { lastEventLog: () => EventLog }).lastEventLog(),
+ );
+ const inputs = log.events.filter((event) => event.type === "input");
+ expect(inputs).toHaveLength(batch.length);
+ expect(
+ inputs
+ .map((event) => ("data" in event.data ? event.data.data : ""))
+ .join(""),
+ ).toBe(batch);
+ expect(inputs.at(-1)?.data.wordIndex).toBe(0);
+});
From 4a59d28938e474ede0e5fdabaa0db5590731249a Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:31:26 +0530
Subject: [PATCH 18/93] ci(engine): verify Rust path routing and allow the
requested PR stack
---
.github/workflows/oxytype-ci.yml | 6 +++---
packages/release/__tests__/ci-routing.spec.js | 14 ++++++++++++++
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/oxytype-ci.yml b/.github/workflows/oxytype-ci.yml
index f4db0d57d..1834d4e31 100644
--- a/.github/workflows/oxytype-ci.yml
+++ b/.github/workflows/oxytype-ci.yml
@@ -9,7 +9,7 @@ permissions:
on:
pull_request:
- branches: [main, "stack/**"]
+ branches: [main, "stack/**", "t3/rust-engine-migration"]
types: [opened, reopened, synchronize, ready_for_review]
push:
branches: [main]
@@ -184,10 +184,10 @@ jobs:
enable start-browser "${runtime[@]}" frontend start-tests browser-tests static-delivery
enable start-updates "${runtime[@]}" frontend start-tests update-tests static-delivery
enable start-delivery "${runtime[@]}" frontend static-delivery
- enable packages packages lint-config static-layouts static-challenges
+ enable packages packages lint-config static-layouts static-challenges engine
enable assets assets runtime-packages
enable code code
- enable engine engine runtime-packages browser-tests
+ enable engine engine runtime-packages
engine:
name: engine
diff --git a/packages/release/__tests__/ci-routing.spec.js b/packages/release/__tests__/ci-routing.spec.js
index f34104a69..10f3f10be 100644
--- a/packages/release/__tests__/ci-routing.spec.js
+++ b/packages/release/__tests__/ci-routing.spec.js
@@ -69,6 +69,7 @@ const allJobs = [
"packages",
"assets",
"code",
+ "engine",
];
const appJobs = [
"backend-test",
@@ -116,6 +117,19 @@ it("runs every job for force-full-ci and main or manual runs", () => {
expect(selected([], { full: true })).toEqual(allJobs);
});
+it.each([
+ "Cargo.toml",
+ "Cargo.lock",
+ "rust-toolchain.toml",
+ "crates/typing-engine/src/session.rs",
+ "crates/typing-engine-wasm/src/lib.rs",
+ "packages/typing-engine/src/browser.client.ts",
+ "packages/typing-fixtures/commands/finite-edit.json",
+ "scripts/engine.ts",
+])("runs native, Wasm, parity and Cloudflare consumers for %s", (file) => {
+ expect(selected([file])).toEqual(allJobs);
+});
+
it.each([
"README.md",
"docs/CI.md",
From 60e451bb947f72cefa56d54cb8cc713eff2db6bb Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:31:28 +0530
Subject: [PATCH 19/93] feat(engine): port base generation custom sections
shuffle and repeats
---
crates/typing-engine/src/generation.rs | 457 +++++++++++++++++++++++
crates/typing-engine/src/lib.rs | 1 +
crates/typing-engine/tests/generation.rs | 109 ++++++
3 files changed, 567 insertions(+)
create mode 100644 crates/typing-engine/src/generation.rs
create mode 100644 crates/typing-engine/tests/generation.rs
diff --git a/crates/typing-engine/src/generation.rs b/crates/typing-engine/src/generation.rs
new file mode 100644
index 000000000..0f8d490a4
--- /dev/null
+++ b/crates/typing-engine/src/generation.rs
@@ -0,0 +1,457 @@
+use crate::{Text, random::Random};
+use serde::{Deserialize, Serialize};
+use std::collections::VecDeque;
+
+#[derive(Clone, Debug, Default, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct Language {
+ pub name: String,
+ pub words: Vec,
+ pub right_to_left: Option,
+ pub joining_script: bool,
+}
+#[derive(Clone, Debug, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct GenerationConfig {
+ pub mode: String,
+ pub words: usize,
+ pub language: String,
+ pub show_all_lines: bool,
+ pub punctuation: bool,
+ pub numbers: bool,
+ pub lazy_mode: bool,
+ pub british_english: bool,
+ pub funbox: Vec,
+}
+impl Default for GenerationConfig {
+ fn default() -> Self {
+ let config = crate::Config::default();
+ Self {
+ mode: config.mode,
+ words: config.words as usize,
+ language: config.language,
+ show_all_lines: false,
+ punctuation: false,
+ numbers: false,
+ lazy_mode: false,
+ british_english: false,
+ funbox: vec![],
+ }
+ }
+}
+#[derive(Clone, Debug, Deserialize)]
+pub struct CustomLimit {
+ pub mode: String,
+ pub value: usize,
+}
+#[derive(Clone, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomText {
+ pub text: Vec,
+ pub mode: String,
+ pub limit: CustomLimit,
+ pub pipe_delimiter: bool,
+}
+#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct NextWord {
+ pub word: Text,
+ pub word_raw: Text,
+ pub section_index: usize,
+}
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Generated {
+ pub words: Vec,
+ pub section_indexes: Vec,
+ pub has_tab: bool,
+ pub has_newline: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub all_right_to_left: Option,
+ pub all_joining_script: bool,
+}
+pub struct Generator {
+ config: GenerationConfig,
+ language: Language,
+ custom: Option,
+ random: Random,
+ wordset: Vec,
+ ordered: usize,
+ shuffled: Vec,
+ section: VecDeque,
+ section_index: usize,
+ section_history: Vec,
+ previous: Vec,
+ repeating: bool,
+ generated: usize,
+}
+fn case(text: &Text, upper: bool) -> Text {
+ let mut output = vec![];
+ let mut valid = String::new();
+ fn flush(valid: &mut String, output: &mut Vec, upper: bool) {
+ let transformed = if upper {
+ valid.to_uppercase()
+ } else {
+ valid.to_lowercase()
+ };
+ output.extend(transformed.encode_utf16());
+ valid.clear();
+ }
+ for value in char::decode_utf16(text.0.iter().copied()) {
+ match value {
+ Ok(value) => valid.push(value),
+ Err(error) => {
+ flush(&mut valid, &mut output, upper);
+ output.push(error.unpaired_surrogate());
+ }
+ }
+ }
+ flush(&mut valid, &mut output, upper);
+ Text(output)
+}
+fn clean_previous(text: &Text, apostrophe: bool) -> Text {
+ case(
+ &Text(
+ text.0
+ .iter()
+ .copied()
+ .filter(|unit| {
+ ![46, 63, 33, 34, 58, 45, 44].contains(unit) && !(apostrophe && *unit == 39)
+ })
+ .collect(),
+ ),
+ false,
+ )
+}
+fn collapse_spaces(text: Text) -> Text {
+ let mut result = vec![];
+ for unit in text.0 {
+ if unit != 32 || result.last() != Some(&32) {
+ result.push(unit);
+ }
+ }
+ if result.first() == Some(&32) {
+ result.remove(0);
+ }
+ if result.last() == Some(&32) {
+ result.pop();
+ }
+ Text(result)
+}
+impl Generator {
+ pub fn new(
+ config: GenerationConfig,
+ language: Language,
+ custom: Option,
+ random: Random,
+ ) -> Result {
+ if !["time", "words", "custom", "zen", "quote"].contains(&config.mode.as_str()) {
+ return Err("Invalid mode".into());
+ }
+ if config.mode == "quote" {
+ return Err("Quote asset selection is not yet ported".into());
+ }
+ if config.punctuation
+ || config.numbers
+ || config.lazy_mode
+ || config.british_english
+ || !config.funbox.is_empty()
+ {
+ return Err("Requested generator transform is not yet ported".into());
+ }
+ if config.mode == "custom" && custom.is_none() {
+ return Err("Missing custom text".into());
+ }
+ if let Some(custom) = &custom
+ && (!["repeat", "random", "shuffle"].contains(&custom.mode.as_str())
+ || !["word", "time", "section"].contains(&custom.limit.mode.as_str()))
+ {
+ return Err("Invalid custom mode or limit".into());
+ }
+ let wordset = if config.mode == "custom" {
+ custom.as_ref().unwrap().text.clone()
+ } else {
+ language.words.clone()
+ };
+ if config.mode != "zen" && (wordset.is_empty() || wordset.iter().all(Text::is_empty)) {
+ return Err("Content has no words".into());
+ }
+ if wordset.len() > 1_000_000 || wordset.iter().any(|word| word.len() > 1_000_000) {
+ return Err("Content exceeds generation bound".into());
+ }
+ Ok(Self {
+ config,
+ language,
+ custom,
+ random,
+ wordset,
+ ordered: 0,
+ shuffled: vec![],
+ section: VecDeque::new(),
+ section_index: 0,
+ section_history: vec![],
+ previous: vec![],
+ repeating: false,
+ generated: 0,
+ })
+ }
+ fn limit(&self) -> usize {
+ if self.config.mode == "zen" {
+ return 0;
+ }
+ let mut limit = 100;
+ if self.config.show_all_lines && self.config.mode == "words" {
+ limit = self.config.words;
+ }
+ if self.config.mode == "words" && self.config.words == 0 {
+ limit = 100;
+ }
+ if let Some(custom) = &self.custom
+ && self.config.mode == "custom"
+ {
+ limit = if custom.limit.value == 0 || custom.limit.mode == "time" {
+ 100
+ } else {
+ custom.limit.value.min(100)
+ };
+ }
+ if self.config.mode == "words" && self.config.words > 0 {
+ limit = limit.min(self.config.words);
+ }
+ limit
+ }
+ pub fn all_generated(&self) -> bool {
+ if self.config.mode == "words" {
+ return self.config.words > 0 && self.generated >= self.config.words;
+ }
+ if self.config.mode == "custom"
+ && let Some(custom) = &self.custom
+ {
+ return custom.limit.value > 0
+ && match custom.limit.mode.as_str() {
+ "word" => self.generated >= custom.limit.value,
+ "section" => {
+ self.section_index >= custom.limit.value && self.section.is_empty()
+ }
+ _ => false,
+ };
+ }
+ false
+ }
+ fn random_word(&mut self) -> Result {
+ Ok(self.wordset[self.random.index(self.wordset.len())?].clone())
+ }
+ fn next_ordered(&mut self) -> Text {
+ if self.ordered >= self.wordset.len() {
+ self.ordered = 0;
+ }
+ let word = self.wordset[self.ordered].clone();
+ self.ordered += 1;
+ word
+ }
+ fn next_shuffled(&mut self) -> Result {
+ if self.shuffled.is_empty() {
+ self.shuffled = (0..self.wordset.len()).collect();
+ self.random.shuffle(&mut self.shuffled)?;
+ }
+ Ok(self.wordset[self.shuffled.pop().unwrap()].clone())
+ }
+ pub fn generate(&mut self, repeating: bool) -> Result {
+ self.repeating = repeating;
+ self.ordered = 0;
+ self.shuffled.clear();
+ self.section.clear();
+ self.section_index = 0;
+ self.section_history.clear();
+ self.generated = 0;
+ if !repeating {
+ self.previous.clear();
+ }
+ let limit = self.limit();
+ if limit > 1_000_000 {
+ return Err("Generation request exceeds bound".into());
+ }
+ let mut words = vec![];
+ let mut indexes = vec![];
+ let mut raw: Vec = vec![];
+ if limit > 0 {
+ loop {
+ let next = self.next(
+ words.len(),
+ limit,
+ raw.last(),
+ raw.get(raw.len().wrapping_sub(2)),
+ )?;
+ indexes.push(next.section_index);
+ words.push(next.word.clone());
+ raw.push(next.word_raw);
+ let sections = self.config.mode == "custom"
+ && self
+ .custom
+ .as_ref()
+ .is_some_and(|custom| custom.pipe_delimiter);
+ if (sections
+ && ((self.section.is_empty() && self.section_index >= limit)
+ || words.len() >= 100))
+ || (!sections && words.len() >= limit)
+ {
+ break;
+ }
+ }
+ }
+ self.generated = words.len();
+ self.section_history.clear();
+ let has_tab = words
+ .iter()
+ .chain(&self.wordset)
+ .any(|word| word.0.contains(&9));
+ let has_newline = words
+ .iter()
+ .chain(&self.wordset)
+ .any(|word| word.0.contains(&10));
+ Ok(Generated {
+ words,
+ section_indexes: indexes,
+ has_tab,
+ has_newline,
+ all_right_to_left: self.language.right_to_left,
+ all_joining_script: self.language.joining_script,
+ })
+ }
+ pub fn next(
+ &mut self,
+ index: usize,
+ _bound: usize,
+ previous: Option<&Text>,
+ previous2: Option<&Text>,
+ ) -> Result {
+ if self.repeating
+ && let Some(next) = self.previous.get(index)
+ {
+ self.section_index += 1;
+ self.generated = index + 1;
+ return Ok(next.clone());
+ }
+ let mut word = self.random_word()?;
+ let previous = previous.map(|word| clean_previous(word, false));
+ let previous2 = previous2.map(|word| clean_previous(word, true));
+ if self.section.is_empty() {
+ let custom = self.custom.clone();
+ if self.config.mode == "custom"
+ && custom
+ .as_ref()
+ .is_some_and(|custom| custom.mode == "repeat")
+ {
+ word = self.next_ordered();
+ } else if self.config.mode == "custom"
+ && custom
+ .as_ref()
+ .is_some_and(|custom| custom.mode == "random")
+ && self.wordset.len() < 4
+ {
+ word = self.random_word()?;
+ } else if self.config.mode == "custom"
+ && custom
+ .as_ref()
+ .is_some_and(|custom| custom.mode == "shuffle")
+ {
+ word = self.next_shuffled()?;
+ } else if self.config.mode == "custom"
+ && custom
+ .as_ref()
+ .is_some_and(|custom| custom.limit.mode == "section")
+ {
+ word = self.random_word()?;
+ let mut retries = 0;
+ while retries < 100
+ && (self.section_history.last() == Some(&word)
+ || self
+ .section_history
+ .get(self.section_history.len().wrapping_sub(2))
+ == Some(&word))
+ {
+ retries += 1;
+ word = self.random_word()?;
+ }
+ } else {
+ let mut retries = 0;
+ let mut first = case(
+ &Text(
+ word.0
+ .split(|unit| *unit == 32)
+ .next()
+ .unwrap_or_default()
+ .to_vec(),
+ ),
+ false,
+ );
+ while retries < 100
+ && (previous.as_ref() == Some(&first)
+ || previous2.as_ref() == Some(&first)
+ || (self.config.mode != "custom"
+ && (word.0 == [73]
+ || (!self.config.language.starts_with("code")
+ && word.0.iter().any(|unit| {
+ b"-=_+[]{};'\\:\"|,./<>?".contains(&(*unit as u8))
+ && *unit < 128
+ }))
+ || word.0.iter().any(|unit| (48..=57).contains(unit)))))
+ {
+ retries += 1;
+ word = self.random_word()?;
+ first = Text(
+ word.0
+ .split(|unit| *unit == 32)
+ .next()
+ .unwrap_or_default()
+ .to_vec(),
+ );
+ }
+ }
+ word = collapse_spaces(word);
+ self.section = word
+ .0
+ .split(|unit| *unit == 32)
+ .map(|part| Text(part.to_vec()))
+ .collect();
+ self.section_history.push(word);
+ self.section_index += 1;
+ }
+ word = self.section.pop_front().ok_or("Empty generated section")?;
+ if word.is_empty() {
+ return Err("Random word is empty".into());
+ }
+ if self.config.mode != "custom"
+ && word.0.iter().any(|unit| (65..=90).contains(unit))
+ && !["german", "swiss_german", "code", "klingon"]
+ .iter()
+ .any(|prefix| self.config.language.starts_with(prefix))
+ {
+ word = case(&word, false);
+ }
+ word = collapse_spaces(word);
+ if self.config.language.starts_with("swiss_german") {
+ let mut out = vec![];
+ for unit in word.0 {
+ if unit == 0xdf {
+ out.extend([115, 115]);
+ } else {
+ out.push(unit);
+ }
+ }
+ word = Text(out);
+ }
+ let mut committed = word.clone();
+ if committed.0.last() != Some(&10) {
+ committed.0.push(32);
+ }
+ let next = NextWord {
+ word: committed,
+ word_raw: word,
+ section_index: self.section_index,
+ };
+ self.previous.push(next.clone());
+ self.generated = index + 1;
+ Ok(next)
+ }
+}
diff --git a/crates/typing-engine/src/lib.rs b/crates/typing-engine/src/lib.rs
index 34375a90b..140ae1280 100644
--- a/crates/typing-engine/src/lib.rs
+++ b/crates/typing-engine/src/lib.rs
@@ -1,4 +1,5 @@
//! Platform-independent typing rules. Hosts provide content, observations and time.
+pub mod generation;
pub mod pace;
pub mod random;
pub mod session;
diff --git a/crates/typing-engine/tests/generation.rs b/crates/typing-engine/tests/generation.rs
new file mode 100644
index 000000000..f9434ee78
--- /dev/null
+++ b/crates/typing-engine/tests/generation.rs
@@ -0,0 +1,109 @@
+use serde_json::{Value, json};
+use typing_engine::{
+ Text,
+ generation::{CustomText, GenerationConfig, Generator, Language},
+ random::Random,
+};
+
+fn neutral_words(value: &mut Value) {
+ if let Some(words) = value.get_mut("words").and_then(Value::as_array_mut) {
+ for word in words {
+ if let Some(value) = word.as_str() {
+ *word = json!(Text::from(value));
+ }
+ }
+ }
+ if let Some(text) = value.get_mut("text").and_then(Value::as_array_mut) {
+ for word in text {
+ if let Some(value) = word.as_str() {
+ *word = json!(Text::from(value));
+ }
+ }
+ }
+}
+#[test]
+fn base_generation_matches_immutable_browser_snapshots() {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
+ let cases: Vec = serde_json::from_slice(
+ &std::fs::read(root.join("packages/typing-fixtures/reference/words/cases.json")).unwrap(),
+ )
+ .unwrap();
+ let mut checked = 0;
+ for case in cases {
+ let config: GenerationConfig = serde_json::from_value(case["config"].clone()).unwrap();
+ if config.punctuation
+ || config.numbers
+ || config.lazy_mode
+ || config.british_english
+ || !config.funbox.is_empty()
+ || config.mode == "quote"
+ {
+ continue;
+ }
+ let name = case["name"].as_str().unwrap();
+ let mut language: Value = serde_json::from_slice(
+ &std::fs::read(root.join(format!(
+ "frontend/static/languages/{}.json",
+ config.language
+ )))
+ .unwrap(),
+ )
+ .unwrap();
+ neutral_words(&mut language);
+ let language: Language = serde_json::from_value(language).unwrap();
+ let custom = case.get("customText").map(|value| {
+ let mut value = value.clone();
+ neutral_words(&mut value);
+ serde_json::from_value::(value).unwrap()
+ });
+ let mut generator = Generator::new(
+ config,
+ language,
+ custom,
+ Random::seeded(case["seed"].as_u64().unwrap() as u32),
+ )
+ .unwrap();
+ let mut generated = generator.generate(false).unwrap();
+ if case["repeat"] == true {
+ generated = generator.generate(true).unwrap();
+ }
+ let mut words = generated.words.clone();
+ let mut extra = vec![];
+ for _ in 0..case["extraWords"].as_u64().unwrap_or(0) {
+ let strip = |text: &Text| {
+ let mut text = text.clone();
+ if matches!(text.0.last(), Some(32 | 10)) {
+ text.0.pop();
+ }
+ text
+ };
+ let previous = words.last().map(strip);
+ let previous2 = words.get(words.len().wrapping_sub(2)).map(strip);
+ let next = generator
+ .next(words.len(), 100, previous.as_ref(), previous2.as_ref())
+ .unwrap();
+ words.push(next.word.clone());
+ extra.push(json!(next));
+ }
+ let mut actual = json!(generated);
+ actual["extra"] = json!(extra);
+ actual["allWordsGenerated"] = json!(generator.all_generated());
+ actual["currentQuote"] = Value::Null;
+ let mut expected: Value = serde_json::from_slice(
+ &std::fs::read(root.join(format!(
+ "packages/typing-fixtures/reference/words/__snapshots__/{name}.json"
+ )))
+ .unwrap(),
+ )
+ .unwrap();
+ neutral_words(&mut expected);
+ for extra in expected["extra"].as_array_mut().unwrap() {
+ for field in ["word", "wordRaw"] {
+ extra[field] = json!(Text::from(extra[field].as_str().unwrap()));
+ }
+ }
+ assert_eq!(actual, expected, "{name}");
+ checked += 1;
+ }
+ assert!(checked >= 15, "Only {checked} cases exercised");
+}
From 0899e247a98005f00fc30d5906fe2d833b558634 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:31:54 +0530
Subject: [PATCH 20/93] fix(engine): preserve browser deletion extents and
Firefox sentinel regressions
---
crates/typing-engine/src/session.rs | 63 ++++++++++++++++------
crates/typing-engine/tests/observations.rs | 45 ++++++++++++++++
2 files changed, 91 insertions(+), 17 deletions(-)
diff --git a/crates/typing-engine/src/session.rs b/crates/typing-engine/src/session.rs
index e12d4059a..5cf4b7147 100644
--- a/crates/typing-engine/src/session.rs
+++ b/crates/typing-engine/src/session.rs
@@ -75,11 +75,16 @@ pub enum Command {
#[serde(default)]
is_composition_ending: Option,
},
+ #[serde(rename_all = "camelCase")]
Delete {
kind: String,
now: f64,
#[serde(default)]
date: f64,
+ #[serde(default)]
+ observed_input: Option,
+ #[serde(default)]
+ sentinel_removed: bool,
},
Start {
now: f64,
@@ -329,11 +334,23 @@ impl Session {
}
self.completion_checkpoint = true;
}
- Command::Delete { kind, now, .. } => {
+ Command::Delete {
+ kind,
+ now,
+ observed_input,
+ sentinel_removed,
+ ..
+ } => {
if !["deleteContentBackward", "deleteWordBackward"].contains(&kind.as_str()) {
return Err("Invalid deletion kind".into());
}
- self.delete(&kind, now);
+ if observed_input.as_ref().is_some_and(|observed| {
+ !self.current().0.starts_with(&observed.0)
+ || (sentinel_removed && !observed.is_empty())
+ }) {
+ return Err("Invalid deletion observation".into());
+ }
+ self.delete(&kind, now, observed_input, sentinel_removed);
}
Command::Start { now, date } => self.start(now, date),
Command::Finish { now, date, reason } => self.end(now, date, reason),
@@ -660,7 +677,7 @@ impl Session {
}
}
- fn delete(&mut self, kind: &str, now: f64) {
+ fn delete(&mut self, kind: &str, now: f64, observed: Option, sentinel_removed: bool) {
if !self.active || self.ended {
return;
}
@@ -681,11 +698,18 @@ impl Session {
{
return;
}
+ let after = observed.unwrap_or_else(|| {
+ if kind == "deleteWordBackward" {
+ delete_word(&input)
+ } else {
+ Text(input.0[..input.len().saturating_sub(1)].to_vec())
+ }
+ });
let unindent = self.config.language.starts_with("code")
&& self.config.code_unindent_on_backspace
&& !input.is_empty()
&& input.0.iter().all(|c| *c == 9)
- && self.target().0.starts_with(&input.0);
+ && self.target().0.starts_with(&after.0);
if unindent {
self.log_delete(
"deleteWordBackward",
@@ -695,21 +719,18 @@ impl Session {
false,
);
}
- let next = if input.is_empty() || unindent {
- self.index = self.index.saturating_sub(1);
- self.previous_input(kind)
- } else if kind == "deleteWordBackward" {
- delete_word(&input)
- } else {
- let mut next = input.clone();
- next.0.pop();
- next
- };
- let char_index = if input.is_empty() || unindent {
- next.len()
+ let regression = input.is_empty() || unindent || sentinel_removed;
+ let next = if regression {
+ if self.index == 0 {
+ Text::default()
+ } else {
+ self.index -= 1;
+ self.previous_input(kind)
+ }
} else {
- input.len()
+ after
};
+ let char_index = if regression { next.len() } else { input.len() };
self.log_delete(
if unindent {
"deleteContentBackward"
@@ -721,6 +742,14 @@ impl Session {
next,
false,
);
+ if sentinel_removed && !unindent && !input.is_empty() {
+ if let Some(event) = self.events.last_mut() {
+ event.data["clearedNextWord"] = json!(true);
+ }
+ if let Some(input) = self.input.get_mut(self.index + 1) {
+ input.0.clear();
+ }
+ }
}
fn advance(&mut self, now: f64, date: f64) {
diff --git a/crates/typing-engine/tests/observations.rs b/crates/typing-engine/tests/observations.rs
index a618abb05..2c3f2c902 100644
--- a/crates/typing-engine/tests/observations.rs
+++ b/crates/typing-engine/tests/observations.rs
@@ -79,3 +79,48 @@ fn composition_defers_difficulty_until_the_committed_end() {
assert_eq!(session.state().reason.as_deref(), Some("difficulty"));
assert_eq!(session.log()["events"][0]["data"]["wordIndex"], 0);
}
+
+#[test]
+fn browser_sentinel_regression_records_destination_and_clears_next_word() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":3})).unwrap(),
+ vec![Text::from("one "), Text::from("two "), Text::from("three")],
+ )
+ .unwrap();
+ apply(
+ &mut session,
+ json!({"op":"insert","data":[120,32,63],"now":0}),
+ );
+ let update = apply(
+ &mut session,
+ json!({"op":"delete","kind":"deleteWordBackward","observedInput":[],"sentinelRemoved":true,"now":20}),
+ );
+ assert_eq!(update.state.word_index, 0);
+ assert!(update.state.input_value.is_empty());
+ assert_eq!(
+ update.events[0].data,
+ json!({"inputType":"deleteWordBackward","wordIndex":0,"charIndex":0,"inputValue":[],"clearedNextWord":true})
+ );
+}
+
+#[test]
+fn unicode_deletion_observation_preserves_browser_grapheme_extent() {
+ let mut session = Session::new(
+ serde_json::from_value(json!({"mode":"words","words":1})).unwrap(),
+ vec![Text::from("😀abc")],
+ )
+ .unwrap();
+ apply(
+ &mut session,
+ json!({"op":"insert","data":[55357,56832],"now":0}),
+ );
+ let update = apply(
+ &mut session,
+ json!({"op":"delete","kind":"deleteContentBackward","observedInput":[],"now":20}),
+ );
+ assert!(update.state.input_value.is_empty());
+ assert_eq!(update.events[0].data["charIndex"], 2);
+ let before = session.log();
+ assert!(session.apply(serde_json::from_value(json!({"op":"delete","kind":"deleteContentBackward","observedInput":[120],"now":30})).unwrap()).is_err());
+ assert_eq!(session.log(), before);
+}
From 7107cdc91d0b81afcf848a630945eead5141e487 Mon Sep 17 00:00:00 2001
From: Lakshmi Tanmay
Date: Fri, 23 Oct 2026 00:40:30 +0530
Subject: [PATCH 21/93] feat(engine): port numeric and Unicode funbox
transforms against frozen references
---
crates/typing-engine/src/transforms.rs | 195 +++++
crates/typing-engine/tests/transforms.rs | 29 +
.../scripts/capture-transform-oracle.ts | 71 ++
.../typing-fixtures/oracle/transforms.json | 796 ++++++++++++++++++
4 files changed, 1091 insertions(+)
create mode 100644 crates/typing-engine/src/transforms.rs
create mode 100644 crates/typing-engine/tests/transforms.rs
create mode 100644 packages/typing-core/scripts/capture-transform-oracle.ts
create mode 100644 packages/typing-fixtures/oracle/transforms.json
diff --git a/crates/typing-engine/src/transforms.rs b/crates/typing-engine/src/transforms.rs
new file mode 100644
index 000000000..9ae8adc5e
--- /dev/null
+++ b/crates/typing-engine/src/transforms.rs
@@ -0,0 +1,195 @@
+use crate::{Text, generation::case, random::Random};
+
+fn append(word: &mut Text, text: &str) {
+ word.0.extend(text.encode_utf16());
+}
+pub fn numbers(random: &mut Random, max: usize) -> Result {
+ let length = random.index(max)? + 1;
+ let mut units = vec![];
+ for index in 0..length {
+ units.push(
+ 48 + (if index == 0 {
+ random.index(9)? + 1
+ } else {
+ random.index(10)?
+ }) as u16,
+ );
+ }
+ Ok(Text(units))
+}
+pub fn numeral_script(word: Text, language: &str) -> Text {
+ let base = if language.starts_with("kurdish") {
+ 0x660
+ } else if language.starts_with("nepali") || language.starts_with("hindi") {
+ 0x966
+ } else if language.starts_with("bangla") {
+ 0x9e6
+ } else {
+ 48
+ };
+ Text(
+ word.0
+ .into_iter()
+ .map(|unit| {
+ if (48..=57).contains(&unit) {
+ base + unit - 48
+ } else {
+ unit
+ }
+ })
+ .collect(),
+ )
+}
+pub fn generated_word(name: &str, random: &mut Random) -> Result