Skip to content

bug: explicit corrections can fail to learn against the visible reading #22

Closed

Description

@snowykr

Regression

PR #18 correctly made plain confirmation preserve the visible reading, but its explicit-correction learning branch still uses hidden final scoring as the baseline. An unchanged word can visibly be Japanese while final scoring prefers English. Choosing English then commits correctly but is not learned; cycling back to the original Japanese can instead create a false correction.

I introduced the retained final-scoring exception in #18. The display-preserving confirmation contract should stay; the correction baseline needs bounded provenance, not a rollback or blanket replacement of final scoring.

Reproduction

Use a fresh persistent synthetic store, with English, Korean and Japanese enabled and live conversion disabled:

  1. Type siasia: the preview is しあしあ.
  2. Use Esc to select the literal English siasia, then Enter. The number of Esc presses depends on the configured language order.
  3. Repeat in a new session using the same store.
  4. Reopen the engine/store and type it again.

Expected: first English correction becomes pending, the second is learned, and the reopened engine shows English.

Actual on v0.10.10: both confirmations insert English, but pending/learned remain empty and reopening still shows Japanese. ozzy (おっzy) has the same problem. A full preconfirmation cycle back to the original Japanese also creates an unnecessary pending Japanese correction.

Minimal Rust test

Save as crates/core/tests/public_origin_repro.rs, then run:

cargo test -p tacit-core --all-features --test public_origin_repro -- --nocapture
use tacit_core::session::testing::FakeTextField;
use tacit_core::{make_engine, AppPolicies, InputSession, Key, Language, Languages};

#[test]
fn explicit_english_correction_learns_the_visible_japanese_origin() {
    let path = std::env::temp_dir().join(format!("tacit-public-origin-{}.json", std::process::id()));
    assert!(!path.exists(), "use a fresh synthetic store");
    let engine = make_engine(Some(path.clone()), &Languages::legacy(true));
    for round in 1..=2 {
        let mut session = InputSession::new(engine.clone(), AppPolicies::default(), true);
        let mut field = FakeTextField::new();
        for ch in "siasia".chars() { session.handle(Key::Letter(ch), &mut field); }
        assert_eq!(field.text(), "しあしあ");
        // This legacy profile orders languages en, ko, ja; other orders can need more Esc presses.
        session.handle(Key::Escape, &mut field);
        assert_eq!(field.text(), "siasia");
        session.handle(Key::Enter, &mut field);
        assert_eq!(field.text(), "siasia");
        if round == 1 {
            assert_eq!(engine.pending().get("siasia"), Some(&Language::English));
        } else {
            assert_eq!(engine.learned().get("siasia"), Some(&Language::English));
        }
    }
    drop(engine);
    let reopened = make_engine(Some(path.clone()), &Languages::legacy(true));
    assert_eq!(reopened.learned().get("siasia"), Some(&Language::English));
    let mut session = InputSession::new(reopened.clone(), AppPolicies::default(), true);
    let mut field = FakeTextField::new();
    for ch in "siasia".chars() { session.handle(Key::Letter(ch), &mut field); }
    assert_eq!(field.text(), "siasia");
    drop(session);
    drop(reopened);
    std::fs::remove_file(path).unwrap();
}

The snippet was executed against the exact v0.10.10 source and fails at the first pending-English assertion.

Revision controls and cause

The forced branch in committed_language chooses decide(false) for learning. That can make the explicit English choice equal the hidden baseline, despite correcting a Japanese display.

The repair should retain the first actually shown language across unchanged cycles, invalidate it on key/context changes, and preserve current-token final scoring after edits/restoration. Before-digit confirmation must retain its existing confidence policy. Japanese Space's separate recording path also needs coverage, without changing learning delivery/dedup timing.

These are core/session and C ABI observations on macOS, not physical-key timing, representative error-frequency measurements, or independent Windows/Linux runtime results. No personal learning data is involved.

Activity

  1. added a commit that references this issue on Oct 24, 2026
    5a7c29f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions