feat: add sticky error-aware key mastery
Persist ranked-only mastery and use it consistently across progression, milestones, counts, and localized UI. Preserve mastery during history rebuilds, replay errors accurately, and generate replay-derived test profiles. Document the follow-up plan to remove the drill history cap.
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
||||
|
||||
use keydr::engine::key_stats::KeyStatsStore;
|
||||
use keydr::engine::ngram_stats::{
|
||||
BigramKey, BigramStatsStore, extract_ngram_events,
|
||||
};
|
||||
use keydr::engine::ngram_stats::{BigramKey, BigramStatsStore, extract_ngram_events};
|
||||
use keydr::session::result::KeyTime;
|
||||
|
||||
fn make_keystrokes(count: usize) -> Vec<KeyTime> {
|
||||
@@ -53,6 +51,10 @@ fn bench_focus_selection(c: &mut Criterion) {
|
||||
let mut bigram_stats = BigramStatsStore::default();
|
||||
let mut char_stats = KeyStatsStore::default();
|
||||
|
||||
// Char baselines: 5% error rate, 430ms per char. Expected independent bigram
|
||||
// error rate is therefore 1 - 0.95^2 = 0.0975, so a bigram needs an error_rate_ema
|
||||
// above ~0.146 to clear the 1.5x error-anomaly ratio threshold, and a
|
||||
// filtered_time_ms above ~645 to clear the 50% speed-anomaly threshold.
|
||||
for &ch in &all_chars {
|
||||
let stat = char_stats.stats.entry(ch).or_default();
|
||||
stat.confidence = 0.8;
|
||||
@@ -60,6 +62,7 @@ fn bench_focus_selection(c: &mut Criterion) {
|
||||
stat.sample_count = 50;
|
||||
stat.total_count = 50;
|
||||
stat.error_count = 3;
|
||||
stat.error_rate_ema = 0.05;
|
||||
}
|
||||
|
||||
let mut count: usize = 0;
|
||||
@@ -70,10 +73,15 @@ fn bench_focus_selection(c: &mut Criterion) {
|
||||
}
|
||||
let key = BigramKey([a, b]);
|
||||
let stat = bigram_stats.stats.entry(key).or_default();
|
||||
stat.confidence = 0.5 + (count % 50) as f64 * 0.01;
|
||||
// Spread values so roughly half the entries clear each anomaly threshold,
|
||||
// exercising both the error and speed candidate paths.
|
||||
stat.error_rate_ema = 0.05 + (count % 20) as f64 * 0.01;
|
||||
stat.filtered_time_ms = 500.0 + (count % 40) as f64 * 10.0;
|
||||
stat.sample_count = 25 + count % 30;
|
||||
stat.error_count = 5 + count % 10;
|
||||
stat.redundancy_streak = if count % 3 == 0 { 3 } else { 1 };
|
||||
// Streak >= 3 confirms an anomaly; mix confirmed and unconfirmed entries.
|
||||
stat.error_anomaly_streak = if count % 3 == 0 { 3 } else { 1 };
|
||||
stat.speed_anomaly_streak = if count % 4 == 0 { 3 } else { 0 };
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
@@ -81,8 +89,10 @@ fn bench_focus_selection(c: &mut Criterion) {
|
||||
|
||||
let unlocked: Vec<char> = all_chars;
|
||||
|
||||
c.bench_function("weakest_bigram (3K entries)", |b| {
|
||||
b.iter(|| bigram_stats.weakest_bigram(black_box(&char_stats), black_box(&unlocked)))
|
||||
c.bench_function("worst_confirmed_anomaly (3K entries)", |b| {
|
||||
b.iter(|| {
|
||||
bigram_stats.worst_confirmed_anomaly(black_box(&char_stats), black_box(&unlocked))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user