Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
8b9eaed
feat(web): own Rust engine loading and session resources per app
voltcrash Oct 23, 2026
7a8db0c
feat(web): gate typing startup on Wasm readiness and retry failures
voltcrash Oct 23, 2026
554e950
feat(web): project settings from generated Rust engine defaults
voltcrash Oct 23, 2026
569e6e6
feat(web): expose owned preflight and live queries for the input adapter
voltcrash Oct 23, 2026
d5625b9
feat(web): project native session deltas and completion into app-owne…
voltcrash Oct 23, 2026
8196ced
test(web): freeze empty Wikipedia extract retries before source cutover
voltcrash Oct 23, 2026
4640820
feat(engine): identify empty source cleanup without consuming its req…
voltcrash Oct 23, 2026
aceb6e3
feat(web): keep IME decisions and async session generations in Rust
voltcrash Oct 23, 2026
5827ebf
feat(web): prepare native content through cancellable raw asset adapters
voltcrash Oct 23, 2026
ef5141b
fix(engine): preserve final-word batch display observations
voltcrash Oct 23, 2026
84b78d6
fix(engine): transfer previewed content into its session once
voltcrash Oct 23, 2026
6cc73c4
fix(engine): mark omitted quote text optional in bindings
voltcrash Oct 23, 2026
8b6682e
refactor(web): prepare owned word and input projections
voltcrash Oct 23, 2026
131ce97
feat(web): expose native session lifecycle and result projections
voltcrash Oct 23, 2026
7f172da
fix(web): preserve input focus during prepared restarts
voltcrash Oct 23, 2026
76de2e3
fix(web): decode optional native quote text losslessly
voltcrash Oct 23, 2026
06c9037
test(web): isolate legacy recorder characterization during cutover
voltcrash Oct 23, 2026
ea2d7b8
test(web): add optimized Wasm session harness
voltcrash Oct 23, 2026
af262a7
feat(web): run typing input and timing through the Rust session
voltcrash Oct 23, 2026
d8bc602
test(web): verify native browser composition and commit projections
voltcrash Oct 23, 2026
4a371c9
docs(migration): record native website input gate evidence
voltcrash Oct 23, 2026
efb5964
feat(engine): expose stateless restart and batched setting policies
voltcrash Oct 23, 2026
0b94e42
test(engine): freeze catalog reads and retained quote queues
voltcrash Oct 23, 2026
a669b6f
build(engine): converge pinned Wasm size optimization
voltcrash Oct 23, 2026
1ff4f90
test(engine): freeze selected quote catalog normalization
voltcrash Oct 23, 2026
1c7d6ac
feat(engine): share quote catalogs and selection cache ownership
voltcrash Oct 23, 2026
c9d132f
feat(web): use native funbox policies across settings
voltcrash Oct 23, 2026
5de4933
feat(web): delegate restart and no-quit decisions to Rust
voltcrash Oct 23, 2026
13b9aaf
refactor(web): derive typing defaults from generated Rust fields
voltcrash Oct 23, 2026
c8a5e30
feat(web): select practice words through native analysis
voltcrash Oct 23, 2026
a1fabef
test(web): compare optimized Wasm generation to all frozen cases
voltcrash Oct 23, 2026
0ad8b88
feat(web): share native quote catalogs and source recovery
voltcrash Oct 23, 2026
e73c55f
refactor(web): remove generation facade and browser rule hooks
voltcrash Oct 23, 2026
a2bbe33
test(web): verify native catalog reads through Cloudflare build
voltcrash Oct 23, 2026
1676499
fix(web): cancel source-bound input safely across route changes
voltcrash Oct 23, 2026
d336e47
refactor(web): extract SSR-safe settings and preserve native policy r…
voltcrash Oct 23, 2026
b77d783
test(web): retain saved funboxes through engine download and retry
voltcrash Oct 23, 2026
d49ab0e
docs(engine): record website generation and settings gate
voltcrash Oct 23, 2026
4377e15
test(engine): freeze original log inspection at 64 prefixes
voltcrash Oct 23, 2026
af0fcac
feat(engine): expose exact historical inspection without result deriv…
voltcrash Oct 23, 2026
5b5cce1
feat(web): add owned native analysis and replay view projections
voltcrash Oct 23, 2026
f8ae861
feat(web): use native result validity and durable custom-text progress
voltcrash Oct 23, 2026
d7b4a63
feat(web): derive result history and charts from native logs
voltcrash Oct 23, 2026
093900b
feat(web): play and seek historical replays through owned Rust handles
voltcrash Oct 23, 2026
f8fe396
feat(web): inspect historical input and key timing through native que…
voltcrash Oct 23, 2026
4e7537b
test(web): exercise native policies in navigation and bailout controls
voltcrash Oct 23, 2026
15a7588
test(web): verify historical parity and result transport through Clou…
voltcrash Oct 23, 2026
fceec6b
docs(engine): record website result and replay compatibility gate
voltcrash Oct 23, 2026
01180ec
fix(engine): preserve targetless zen cursor projection
voltcrash Oct 23, 2026
c853859
fix(web): retain and precache matched wasm assets
voltcrash Oct 23, 2026
2aa779d
test(web): enforce frozen wasm website performance budgets
voltcrash Oct 23, 2026
16d0148
test(web): verify wasm cache pairing across updates and rollback
voltcrash Oct 23, 2026
24b3a38
docs(engine): record website delivery and performance evidence
voltcrash Oct 23, 2026
0d1af35
test(web): seek replay using a visible word character
voltcrash Oct 23, 2026
184c3fc
test(engine): freeze remaining synthetic analysis parity calls
voltcrash Oct 23, 2026
f896af9
docs(engine): pass website delivery gate and begin retirement
voltcrash Oct 23, 2026
1f69247
test(engine): preserve all remaining browser string rule cases
voltcrash Oct 23, 2026
badc5fc
refactor(web): extract host helpers and legacy wire types
voltcrash Oct 23, 2026
c401089
refactor(web): retire unused rule facades and legacy test recorder
voltcrash Oct 23, 2026
e5f5896
refactor(engine): remove retired TypeScript engine package
voltcrash Oct 23, 2026
19871ed
docs(engine): map final Rust ownership and legacy retirement
voltcrash Oct 23, 2026
1ea5016
docs(web): document matched wasm release and rollback contract
voltcrash Oct 23, 2026
711a5a8
perf(engine): borrow observed key codes during timing analysis
voltcrash Oct 23, 2026
13ae38d
perf(engine): share JSON parsing for immutable British rules
voltcrash Oct 23, 2026
89f9a2f
build(engine): scope wasm cache to runtime and binding inputs
voltcrash Oct 23, 2026
3d7a509
test(web): settle account settings before result submission
voltcrash Oct 23, 2026
748a705
docs(engine): record cross-platform payload failure and parser reduction
voltcrash Oct 24, 2026
4682d0f
perf(web): preload hashed wasm before browser hydration
voltcrash Oct 24, 2026
0b17aab
refactor(web): remove exports left unused after engine retirement
voltcrash Oct 24, 2026
92415b6
docs(engine): retain final website parity and performance measurements
voltcrash Oct 24, 2026
2a5111b
docs(engine): pass website retirement and full CI gate
voltcrash Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 5 additions & 5 deletions .github/workflows/oxytype-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
- 'packages/oxlint-config/**'
# Workspace packages bundled into the backend and frontend.
runtime-packages:
- 'packages/{challenges,contracts,funbox,schemas,typing-core,typing-engine,typing-fixtures,util}/**'
- 'packages/{challenges,contracts,funbox,schemas,typing-engine,typing-fixtures,util,web-shared}/**'
- 'crates/**'
- 'Cargo.{toml,lock}'
- 'rust-toolchain.toml'
Expand All @@ -95,8 +95,8 @@ jobs:
- '!**/*.md'
backend-tests:
- 'backend/__tests__/**'
# Anticheat specs replay typing-core's recorded fixtures.
- 'packages/typing-core/__fixtures__/**'
# Anticheat specs replay the immutable engine reference fixtures.
- 'packages/typing-fixtures/reference/**'
# Backend test data and D1 helpers the frontend unit and SSR suites import.
backend-test-support:
- 'backend/__tests__/{__testData__/**,d1/helpers.ts,d1/result-support.ts}'
Expand All @@ -109,8 +109,8 @@ jobs:
frontend-tests:
- 'frontend/__tests__/**'
- 'frontend/vitest.config.ts'
# Parity specs replay typing-core's recorded fixtures.
- 'packages/typing-core/__fixtures__/**'
# Parity specs replay the immutable engine reference fixtures.
- 'packages/typing-fixtures/reference/**'
- '!frontend/__tests__/start/**'
# Start harness: SSR suites, and the workerd fixture the browser suites serve.
start-tests:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ vp run dev

The TanStack Start app, its API and local D1 run at `http://localhost:3000`.

Typing rules run in the platform-independent [Rust engine](./crates/typing-engine).
The website loads its [Wasm SDK](./packages/typing-engine/README.md) only in the
browser; Cloudflare continues serving SSR, auth and APIs. Install the toolchain
pinned in `rust-toolchain.toml` with its Wasm target. Build bootstrap generates the
bindings automatically; `vp run engine:parity` checks native and built-browser
behavior against immutable original references. `vp run engine:lint` and
`vp run engine:fmt` check Rust. Reference provenance and measured budgets are in
[migration progress](./docs/rust-engine/progress.md).

## Contributing

See [CONTRIBUTING.md](./docs/CONTRIBUTING.md) and the [code of conduct](./docs/CODE_OF_CONDUCT.md). Report bugs in the [Oxytype repository](https://github.com/voltcrash/oxytype/issues).
Expand Down
2 changes: 1 addition & 1 deletion backend/__tests__/anticheat/detectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {

const fixtures = resolve(
__dirname,
"../../../packages/typing-core/__fixtures__",
"../../../packages/typing-fixtures/reference",
);

function load(path: string): TimingSample[] {
Expand Down
4 changes: 2 additions & 2 deletions backend/scripts/anticheat-calibrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ recordings and synthetic fixtures are used.`;
const root = resolve(__dirname, "../..");
const DEFAULT_SETS: [string, string][] = [
// scripted Playwright input against the real web client, not human typing
["recorded-web", "packages/typing-core/__fixtures__/keystrokes"],
["recorded-web", "packages/typing-fixtures/reference/keystrokes"],
// label comes from each file: bot or model
["", "packages/typing-core/__fixtures__/anticheat"],
["", "packages/typing-fixtures/reference/anticheat"],
];

async function files(path: string): Promise<string[]> {
Expand Down
51 changes: 44 additions & 7 deletions crates/typing-engine-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ impl WasmSession {
.ok_or_else(|| failure(ErrorCode::Disposed, "Session disposed"))?;
serialize(&session.live_stats(now).map_err(bind_error)?)
}
pub fn composition_completes(&self, data: &str) -> Result<bool, JsError> {
let data = parse(data)?;
let session = self
.session
.as_ref()
.ok_or_else(|| failure(ErrorCode::Disposed, "Session disposed"))?;
session.composition_completes(&data).map_err(bind_error)
}
pub fn can_delete(&self) -> Result<bool, JsError> {
let session = self
.session
.as_ref()
.ok_or_else(|| failure(ErrorCode::Disposed, "Session disposed"))?;
Ok(session.can_delete())
}
pub fn word_burst(&self, index: usize, now: f64) -> Result<f64, JsError> {
let session = self
.session
.as_ref()
.ok_or_else(|| failure(ErrorCode::Disposed, "Session disposed"))?;
session.word_burst(index, now).map_err(bind_error)
}
pub fn snapshot(&self) -> Result<String, JsError> {
let session = self
.session
Expand Down Expand Up @@ -148,6 +170,22 @@ impl WasmContent {
.prepare_repeating(input, repeating)
.map_err(bind_error)
}
// Reuse the existing prepared-asset envelope/parser. Only quotes, random and
// quote lengths participate; other preparation fields do not affect catalogs.
pub fn quote_catalog(&mut self, input: &str, refresh_lengths: bool) -> Result<String, JsError> {
let input: typing_engine::generation::Prepare = parse(input)?;
let asset = input
.quotes
.ok_or_else(|| failure(ErrorCode::InvalidContent, "Quote catalog asset missing"))?;
let lengths = refresh_lengths.then_some(input.config.quote_length);
let collection = self
.content
.as_mut()
.ok_or_else(|| failure(ErrorCode::Disposed, "Content disposed"))?
.quote_catalog(asset, lengths, input.random)
.map_err(bind_error)?;
serialize(&collection)
}
pub fn update_quote_queue(&mut self, lengths: &str, random: &str) -> Result<String, JsError> {
let lengths: Vec<i32> = parse(lengths)?;
let random = parse(random)?;
Expand All @@ -170,13 +208,12 @@ impl WasmContent {
.map_err(bind_error)
}
pub fn generate(&mut self, repeating: bool) -> Result<String, JsError> {
let generator = self
let generated = self
.content
.as_mut()
.ok_or_else(|| failure(ErrorCode::Disposed, "Content disposed"))?
.generator()
.generate(repeating)
.map_err(bind_error)?;
let generated = generator.generate(repeating).map_err(bind_error)?;
serialize(&generated)
}
pub fn next(
Expand All @@ -202,9 +239,9 @@ impl WasmContent {
pub fn metadata(&mut self) -> Result<String, JsError> {
let generator = self
.content
.as_mut()
.as_ref()
.ok_or_else(|| failure(ErrorCode::Disposed, "Content disposed"))?
.generator()
.prepared_generator()
.map_err(bind_error)?;
serialize(
&serde_json::json!({ "allWordsGenerated": generator.all_generated(), "currentQuote": generator.current_quote }),
Expand All @@ -213,9 +250,9 @@ impl WasmContent {
pub fn request(&mut self) -> Result<String, JsError> {
let generator = self
.content
.as_mut()
.as_ref()
.ok_or_else(|| failure(ErrorCode::Disposed, "Content disposed"))?
.generator()
.prepared_generator()
.map_err(bind_error)?;
serialize(&generator.pending_request())
}
Expand Down
43 changes: 43 additions & 0 deletions crates/typing-engine/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum Query {
Frame {
time: f64,
},
Inspect,
CustomProgress {
previous: u32,
},
Expand Down Expand Up @@ -53,6 +54,9 @@ pub enum Response {
Frame {
frame: replay::Frame,
},
Inspect {
inspection: Box<Inspection>,
},
CustomProgress {
progress: crate::custom_progress::LongProgress,
},
Expand All @@ -67,6 +71,8 @@ pub struct Overview {
pub input_history: Vec<Text>,
pub corrected_words_history: Vec<Text>,
pub missed_words: Vec<(Text, usize)>,
// serde_json writes positive infinity as null; the browser restores its display value.
#[ts(type = "Array<number | null>")]
pub word_burst_history: Vec<f64>,
pub raw_history: Vec<f64>,
pub keypresses_per_second: Vec<usize>,
Expand All @@ -77,6 +83,40 @@ pub struct Overview {
pub date_duration_ms: f64,
pub word_indexes_per_second: Vec<Vec<usize>>,
}
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Inspection {
pub accuracy: stats::Accuracy,
pub key_spacing: Vec<f64>,
pub key_duration: Vec<f64>,
pub boundary_labels: Vec<String>,
pub inputs: Vec<Text>,
pub word_index: i32,
}
fn inspect(log: &Log) -> Inspection {
let timing = stats::key_timing(log);
let mut inputs = vec![Text::default(); log.context.target_words.len()];
let mut word_index = -1;
// Total observed input text is bounded before this scan. Preserve each last
// DOM snapshot, including legacy final-commit trim, without a second index.
for event in &log.events {
if event.kind == "input" {
let index = stats::word_index(event).expect("Validated input index");
if let Some(value) = inputs.get_mut(index) {
*value = stats::input_value(&event.data);
}
word_index = index as i32;
}
}
Inspection {
accuracy: stats::accuracy_counts(log, None),
key_spacing: timing.spacing,
key_duration: timing.durations,
boundary_labels: stats::boundary_labels(log, false),
inputs,
word_index,
}
}
pub fn analyze(log: &Log, query: Query) -> Result<Response, crate::error::EngineError> {
crate::validation::log(log)?;
match &query {
Expand Down Expand Up @@ -138,6 +178,9 @@ pub fn analyze(log: &Log, query: Query) -> Result<Response, crate::error::Engine
steps: replay::steps(log, zen),
duration: replay::duration(log),
},
Query::Inspect => Response::Inspect {
inspection: Box::new(inspect(log)),
},
Query::Frame { time } => Response::Frame {
frame: replay::frame(log, time),
},
Expand Down
1 change: 1 addition & 0 deletions crates/typing-engine/src/bin/export-bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
typing_engine::generation::Generated::export_all(&config)?;
typing_engine::generation::NextWord::export_all(&config)?;
typing_engine::quotes::Quote::export_all(&config)?;
typing_engine::quotes::QuoteCollection::export_all(&config)?;
Ok(())
}
5 changes: 4 additions & 1 deletion crates/typing-engine/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ impl Queue {
}
let section = source.normalize(self.english)?;
if section.words.is_empty() {
return Err("Section has no words".into());
return Err(crate::error::EngineError::new(
crate::error::ErrorCode::EmptyContent,
"Section has no words",
));
}
self.ready.push_back(section);
Ok(())
Expand Down
1 change: 1 addition & 0 deletions crates/typing-engine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum ErrorCode {
Disposed,
BindingMismatch,
ContentRequired,
EmptyContent,
StaleRequest,
NoQuotes,
SelectedQuoteMissing,
Expand Down
45 changes: 42 additions & 3 deletions crates/typing-engine/src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub struct NextWord {
pub word_raw: Text,
pub section_index: usize,
}
#[derive(Debug, Serialize, TS)]
#[derive(Clone, Debug, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Generated {
pub words: Vec<Text>,
Expand Down Expand Up @@ -987,6 +987,7 @@ pub struct Prepare {
/// Retains only explicitly owned content-selection state across restarts.
#[derive(Default)]
pub struct Content {
prepared: Option<(bool, Generated)>,
assets: Assets,
generator: Option<Generator>,
weak_spot: crate::weak_spot::WeakSpot,
Expand All @@ -1008,6 +1009,29 @@ impl Content {
self.generation = self.generation.max(memory.generation);
self.has_prepared = true;
self.generator = None;
self.prepared = None;
}
pub fn quote_catalog(
&mut self,
asset: crate::quotes::QuoteAsset,
lengths: Option<Vec<i32>>,
mut random: Random,
) -> Result<crate::quotes::QuoteCollection, EngineError> {
crate::validation::random(&random)?;
if lengths.as_ref().is_some_and(|lengths| {
lengths.len() > 64
|| lengths
.iter()
.any(|length| ![-3, -2, 0, 1, 2, 3].contains(length))
}) {
return Err(EngineError::new(
ErrorCode::InvalidConfig,
"Invalid quote length selection",
));
}
self.assets
.quotes
.load_catalog(asset, lengths.as_deref(), &mut random)
}
pub fn update_quote_queue(
&mut self,
Expand Down Expand Up @@ -1047,6 +1071,7 @@ impl Content {
}
self.has_prepared = true;
self.generator = None;
self.prepared = None;
self.assets.quote_asset = input.quotes;
self.assets.polyglot = input.polyglot;
self.assets.selected_quote_id = input.selected_quote_id;
Expand Down Expand Up @@ -1077,13 +1102,27 @@ impl Content {
&mut self,
repeating: bool,
) -> Result<crate::Session, crate::error::EngineError> {
let generated = self.generator()?.generate(repeating)?;
let generated = match self.prepared.take() {
Some((prepared_repeat, generated)) if prepared_repeat == repeating => generated,
_ => self.generator()?.generate(repeating)?,
};
let generator = self.generator.take().ok_or("Content not prepared")?;
let mut session = crate::Session::from_generated(generator, generated)?;
session.pace.remember(self.last_pace_wpm, false);
Ok(session)
}
pub fn generator(&mut self) -> Result<&mut Generator, crate::error::EngineError> {
/// Inspect prepared output without restarting generation or consuming RNG/source content.
pub fn generate(&mut self, repeating: bool) -> Result<Generated, EngineError> {
let generated = self.generator()?.generate(repeating)?;
self.prepared = Some((repeating, generated.clone()));
Ok(generated)
}
pub fn prepared_generator(&self) -> Result<&Generator, EngineError> {
self.generator.as_ref().ok_or("Content not prepared".into())
}
/// Mutable generator access invalidates any prior presentation snapshot.
pub fn generator(&mut self) -> Result<&mut Generator, EngineError> {
self.prepared = None;
self.generator.as_mut().ok_or("Content not prepared".into())
}
}
Loading
Loading