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:
2026-08-12 01:09:52 -04:00
parent 0f8493eb02
commit 5daa644d3b
47 changed files with 2683 additions and 722 deletions
+15 -9
View File
@@ -2,8 +2,8 @@ pub use rust_i18n::t;
/// Available UI locale codes. Separate from dictionary language support.
pub const SUPPORTED_UI_LOCALES: &[&str] = &[
"en", "de", "es", "fr", "it", "pt", "nl", "sv", "da", "nb", "fi", "pl", "cs", "ro", "hr",
"hu", "lt", "lv", "sl", "et", "tr",
"en", "de", "es", "fr", "it", "pt", "nl", "sv", "da", "nb", "fi", "pl", "cs", "ro", "hr", "hu",
"lt", "lv", "sl", "et", "tr",
];
pub fn set_ui_locale(locale: &str) {
@@ -18,7 +18,11 @@ pub fn set_ui_locale(locale: &str) {
/// Retrieve the set of all translation keys for a given locale.
/// Used by the catalog parity test to verify every key exists in every locale.
#[cfg(test)]
fn collect_yaml_keys(value: &serde_yaml::Value, prefix: &str, keys: &mut std::collections::BTreeSet<String>) {
fn collect_yaml_keys(
value: &serde_yaml::Value,
prefix: &str,
keys: &mut std::collections::BTreeSet<String>,
) {
match value {
serde_yaml::Value::Mapping(map) => {
for (k, v) in map {
@@ -54,9 +58,7 @@ pub fn localized_language_layout_error(
layout = layout_key
)
.to_string(),
LanguageBlockedBySupportLevel(key) => {
t!("errors.language_blocked", key = key).to_string()
}
LanguageBlockedBySupportLevel(key) => t!("errors.language_blocked", key = key).to_string(),
}
}
@@ -67,8 +69,8 @@ mod tests {
fn locale_keys(locale: &str) -> BTreeSet<String> {
let path = format!("locales/{locale}.yml");
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read {path}: {e}"));
let content =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("Failed to read {path}: {e}"));
let root: serde_yaml::Value = serde_yaml::from_str(&content)
.unwrap_or_else(|e| panic!("Failed to parse {path}: {e}"));
let mut keys = BTreeSet::new();
@@ -111,7 +113,11 @@ mod tests {
}
}
assert!(errors.is_empty(), "Catalog parity errors:\n{}", errors.join("\n"));
assert!(
errors.is_empty(),
"Catalog parity errors:\n{}",
errors.join("\n")
);
}
#[test]