| 1 | # Plan: Standardized Keystroke Dynamics Spec + Reference Library |
| 2 | |
| 3 | ## Context |
| 4 | |
| 5 | The existing web app (`web/`) has solid keystroke dynamics logic (capture, aggregation, comparison, humanness scoring) but it's coupled to a Svelte UI. The goal is to extract this into a **language-agnostic spec** + **standalone TypeScript reference library** that other languages (Python, Rust, Go) can implement against. Use cases include school enrollment verification and proving humanness in online writing. |
| 6 | |
| 7 | ## Decisions Made |
| 8 | |
| 9 | - **Structure**: New `spec/` and `ts/` directories alongside `web/` |
| 10 | - **Metrics**: 4 essential per digraph (holdTime1, holdTime2, pressPress, releasePress); 2 derivable ones are optional |
| 11 | - **Digraphs**: Hybrid — ~30 core English digraphs + any-digraph fallback with min 5 observations |
| 12 | - **Scope**: Full end-to-end — spec document, reference library with tests, and web app refactor |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Phase 1: Spec Document + JSON Schemas |
| 17 | |
| 18 | ### 1.1 Create `spec/SPEC.md` |
| 19 | |
| 20 | RFC-style markdown document with these sections: |
| 21 | |
| 22 | 1. **Introduction & Scope** — what this covers, versioning (`kd-spec/1.0`) |
| 23 | 2. **Raw Data Capture** — `KeystrokeEvent` and `RawDigraph` definitions, key normalization rules, excluded keys, `SessionMetadata` shape |
| 24 | 3. **Digraph Selection** — core set concept, `spec/digraphs/en.json`, confidence tiers (15+/10-14/5-9/<5 shared core digraphs), minimum observation count (5) |
| 25 | 4. **Aggregation** — group by normalized key pair, IQR outlier filtering (k=1.5), compute MetricStats (mean, std w/ Bessel's correction, min, max, count), round to 0.1ms |
| 26 | 5. **Identity Comparison** — Scaled Manhattan Distance over 4 metrics, `MAX_METRIC_DISTANCE=3.0`, `MIN_STD_FALLBACK=15.0`, similarity % formula, confidence levels |
| 27 | 6. **Humanness Scoring** — 6 weighted sub-scores with exact piecewise-linear breakpoints: |
| 28 | - timingVariance (0.20): CV of pressPress, range [0.03→0, 0.20→100] |
| 29 | - correctionRate (0.15): backspace ratio, ideal 2-15% |
| 30 | - pauseDistribution (0.20): gaps >500ms rate + variance bonus |
| 31 | - distributionShape (0.15): skewness of pressPress, ideal 0.5-3.0 |
| 32 | - flightTimeNegativity (0.15): % negative releasePress, ideal 10-50% |
| 33 | - burstPatterns (0.15): burst length mean + CV (burst = consecutive <300ms gaps) |
| 34 | - Paste penalty when >50% pasted content |
| 35 | - Verdicts: ≥60 likely_human, 40-59 uncertain, <40 likely_bot |
| 36 | 7. **Attestation Object** — JSON shape with specVersion, score, verdict, subScores, sessionStats, optional profileComparison |
| 37 | |
| 38 | ### 1.2 Create JSON Schema files in `spec/schemas/` |
| 39 | |
| 40 | - `raw-keystroke-event.schema.json` |
| 41 | - `raw-digraph.schema.json` |
| 42 | - `session-metadata.schema.json` |
| 43 | - `digraph-aggregation.schema.json` |
| 44 | - `profile.schema.json` |
| 45 | - `comparison-result.schema.json` |
| 46 | - `humanness-result.schema.json` |
| 47 | - `humanness-attestation.schema.json` |
| 48 | |
| 49 | ### 1.3 Create `spec/digraphs/en.json` |
| 50 | |
| 51 | ~30 high-frequency English digraphs: th, he, in, er, an, re, on, at, en, nd, ti, es, or, te, of, ed, is, it, al, ar, st, to, nt, ng, se, ha, ou, le, ve, co |
| 52 | |
| 53 | --- |
| 54 | |
| 55 | ## Phase 2: Reference TypeScript Library (`ts/`) |
| 56 | |
| 57 | ### 2.1 Package setup |
| 58 | |
| 59 | - `ts/package.json` — zero production dependencies, `vitest` for testing |
| 60 | - `ts/tsconfig.json` — ES2023, strict, ESM output |
| 61 | - Exports as both ESM and CJS |
| 62 | |
| 63 | ### 2.2 Source modules (extracted + refactored from `web/src/lib/`) |
| 64 | |
| 65 | | File | Source | Changes | |
| 66 | |---|---|---| |
| 67 | | `ts/src/types.ts` | `web/src/lib/types.ts` | Add `KeystrokeEvent`, `HumannessAttestation`. Remove `TabId`. Make `pressRelease`/`releaseRelease` optional. `DigraphAggregation` uses 4 essential metrics (2 optional). | |
| 68 | | `ts/src/math.ts` | `web/src/lib/utils.ts` | Keep `round`, `std`, `filterOutliers`. Drop `avg` (UI concern). Add proper `mean()` returning number. | |
| 69 | | `ts/src/normalize.ts` | `web/src/lib/aggregation.ts` lines 1-28 | Extract `normalizeKey`, `normalizeDigraphKey`, `EXCLUDED_KEYS`, `shouldExclude`. | |
| 70 | | `ts/src/capture.ts` | `web/src/App.svelte` lines 46-107 | **NEW**: Platform-agnostic `CaptureSession` class with `keyDown(key, timestamp)`, `keyUp(key, timestamp)` → `RawDigraph | null`, `getMetadata()`, `getDigraphs()`, `reset()`. | |
| 71 | | `ts/src/aggregate.ts` | `web/src/lib/aggregation.ts` | `computeMetricStats`, `aggregateDigraphs`. Operate on 4 essential metrics by default. | |
| 72 | | `ts/src/compare.ts` | `web/src/lib/comparison.ts` | `compareSession`. Change `METRIC_KEYS` from 6 to 4. | |
| 73 | | `ts/src/humanness.ts` | `web/src/lib/humanness.ts` | All 6 sub-score functions + `analyzeHumanness`. Consolidate duplicated `computeStd` into `math.ts`. | |
| 74 | | `ts/src/attestation.ts` | NEW | `createAttestation(humanness, digraphs, metadata, comparison?)` → `HumannessAttestation` | |
| 75 | | `ts/src/index.ts` | NEW | Barrel export of public API | |
| 76 | |
| 77 | ### 2.3 Tests (`ts/tests/`) |
| 78 | |
| 79 | - `math.test.ts` — round, std, mean, filterOutliers |
| 80 | - `capture.test.ts` — CaptureSession state machine |
| 81 | - `normalize.test.ts` — key normalization edge cases |
| 82 | - `aggregate.test.ts` — grouping, outlier filtering, metric stats |
| 83 | - `compare.test.ts` — distance calculation, similarity %, confidence levels |
| 84 | - `humanness.test.ts` — each sub-score function + composite scoring |
| 85 | - `fixtures.test.ts` — load spec fixtures, assert outputs match |
| 86 | |
| 87 | --- |
| 88 | |
| 89 | ## Phase 3: Test Fixtures (`spec/fixtures/`) |
| 90 | |
| 91 | Generate from the reference implementation using real profile data (`1.json`, `2.json`): |
| 92 | |
| 93 | - `aggregation/` — input raw digraphs → expected aggregations |
| 94 | - `comparison/` — input session+profile aggregations → expected ComparisonResult |
| 95 | - `humanness/` — human-like session → expected scores; bot-like session → expected scores |
| 96 | - `math/` — known inputs → expected outputs for filterOutliers, std, etc. |
| 97 | |
| 98 | Each fixture: `{ input: {...}, expectedOutput: {...} }` with 0.1 tolerance for floats. |
| 99 | |
| 100 | --- |
| 101 | |
| 102 | ## Phase 4: Refactor Web App |
| 103 | |
| 104 | ### 4.1 Add library dependency |
| 105 | |
| 106 | Add `ts/` as a workspace or path dependency in `web/package.json`. |
| 107 | |
| 108 | ### 4.2 Replace inline modules |
| 109 | |
| 110 | | Web file | Action | |
| 111 | |---|---| |
| 112 | | `web/src/lib/types.ts` | Replace with re-exports from `@keystroke-dynamics/core` (or path import) | |
| 113 | | `web/src/lib/utils.ts` | Replace with imports from library's `math.ts`. Keep `avg()` locally (UI helper). | |
| 114 | | `web/src/lib/aggregation.ts` | Replace with imports from library | |
| 115 | | `web/src/lib/comparison.ts` | Replace with imports from library | |
| 116 | | `web/src/lib/humanness.ts` | Replace with imports from library | |
| 117 | | `web/src/lib/storage.ts` | **Keep as-is** — browser-specific, intentionally not in spec | |
| 118 | | `web/src/App.svelte` | Replace inline capture logic (lines 46-107) with `CaptureSession` instance | |
| 119 | |
| 120 | ### 4.3 Verify |
| 121 | |
| 122 | - `bun run check` passes |
| 123 | - `bun run dev` works, all tabs functional |
| 124 | - Existing profiles (`1.json`, `2.json`) still load and compare correctly |
| 125 | |
| 126 | --- |
| 127 | |
| 128 | ## Key Files to Modify/Create |
| 129 | |
| 130 | **Create:** |
| 131 | - `spec/SPEC.md` |
| 132 | - `spec/schemas/*.schema.json` (8 files) |
| 133 | - `spec/digraphs/en.json` |
| 134 | - `spec/fixtures/` (fixture JSON files) |
| 135 | - `ts/package.json`, `ts/tsconfig.json` |
| 136 | - `ts/src/*.ts` (9 files) |
| 137 | - `ts/tests/*.test.ts` (7 files) |
| 138 | |
| 139 | **Modify:** |
| 140 | - `web/src/lib/types.ts` — re-export from library |
| 141 | - `web/src/lib/utils.ts` — import from library, keep `avg` locally |
| 142 | - `web/src/lib/aggregation.ts` — re-export from library |
| 143 | - `web/src/lib/comparison.ts` — re-export from library |
| 144 | - `web/src/lib/humanness.ts` — re-export from library |
| 145 | - `web/src/App.svelte` — use `CaptureSession` class |
| 146 | - `web/package.json` — add library dependency |
| 147 | |
| 148 | **Keep unchanged:** |
| 149 | - `web/src/lib/storage.ts` |
| 150 | - All Svelte components (they import from `web/src/lib/` which will re-export) |
| 151 | |
| 152 | --- |
| 153 | |
| 154 | ## Verification |
| 155 | |
| 156 | 1. `cd ts && bun test` — all library unit tests pass |
| 157 | 2. `cd ts && bun test fixtures.test.ts` — all spec fixtures pass |
| 158 | 3. `cd web && bun run check` — TypeScript/Svelte checks pass |
| 159 | 4. `cd web && bun run dev` — app runs, type in textarea, verify: |
| 160 | - Capture tab shows digraphs |
| 161 | - Profile tab shows aggregations |
| 162 | - Save a profile, compare against it |
| 163 | - Humanness tab shows score with all 6 sub-metrics |
| 164 | - Import `1.json`/`2.json`, comparison still works |
| 165 | 5. Validate JSON schemas: `npx ajv validate -s spec/schemas/raw-digraph.schema.json -d spec/fixtures/...` |
| 166 | |