Skip to content

fix(dictionary): stop deleted/junk words from resurrecting (incl. via swipe)#43

Merged
AsafMah merged 2 commits into
mainfrom
fix/dictionary-blocklist-resurrection
Jun 5, 2026
Merged

fix(dictionary): stop deleted/junk words from resurrecting (incl. via swipe)#43
AsafMah merged 2 commits into
mainfrom
fix/dictionary-blocklist-resurrection

Conversation

@AsafMah

@AsafMah AsafMah commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Closes #38 (part of #18).

Problem

Deleting a word from suggestions didn't stick - it came back, even when swiping. Concrete case: swiping the Hebrew word לא increasingly yields the non-word לר.

Root cause (traced)

Gesture suggestions use the same native engine + UserHistoryDictionary as typing. The native score is compoundDistance = spatialDistance + languageDistance * weight where languageDistance falls as a word's frequency rises - so a high-frequency learned junk word buys down bad swipe geometry and out-ranks the real word. Three blocklist gaps let the junk word keep coming back:

  1. removeWord only blacklisted words in a read-only dict (main/contacts/apps). A junk word that lived only in user-history was removed but not blacklisted, so it was re-learned on the next commit.
  2. addWordToUserHistory never checked the blacklist before learning.
  3. Every commit unconditionally un-blacklisted the word.

Fix

  1. removeWord now blacklists a word that lived only in the mutable user-history/personal dict. The existing suggestion filter then hides it (incl. the glide path).
  2. addWordToUserHistory early-returns for blacklisted words.
  3. The per-commit un-blacklist only fires for words real in a read-only dict (new DictionaryGroup.isInReadOnlyDict), so typing a real word restores it but committing junk does not.

This matches the "trash a typo forever" intent while preserving legitimate restore-by-retyping for real words. (Graduated learning for never-deleted new words is separate: #39; the flag/Add/Block UI is #40.)

Tests / verification (JDK 21)

  • New DictionaryGroupTest cases: history-only word blacklisted on removal (gap 1); main-dict word still blacklisted (read-only regression); isInReadOnlyDict behavior.
  • Full :app:testOfflineDebugUnitTest: 218 tests, 11 failed vs baseline origin/main 215 tests, 11 failed - the same 11 pre-existing failures (parser/xlink/emoji + 3 known InputLogic, all unrelated), 0 new, my 3 new tests pass.

Reviewer note

Gaps 2 & 3 live in DictionaryFacilitatorImpl which needs the native dictionaries for an end-to-end test; here they are compile-verified + reasoned. A full integration test belongs to the trace/replay harness (#21).

AsafMah added 2 commits June 5, 2026 00:49
… swipe)

A word the user deletes from suggestions could come back -- even when
swiping it -- because of three gaps in the blocklist logic:

- removeWord did not blacklist a word that lived only in the mutable
  user-history / personal dict; it was removed but re-learned on the
  next commit.
- addWordToUserHistory never checked the blacklist before learning.
- every commit unconditionally un-blacklisted the committed word.

Once re-learned, a junk word's user-history frequency lowers its
language cost in the native scorer (compoundDistance = spatialDistance
+ languageDistance * weight, dic_node_state_scoring.h), so it can
out-rank a real word with better gesture geometry -- e.g. swiping the
Hebrew word "לא" increasingly yields the non-word "לר".

Fix (the three gaps):
1. DictionaryGroup.removeWord now blacklists a word that was only in the
   mutable dicts (history/personal). The existing suggestion filter
   (getSuggestions) then hides it, including in the glide-typing path.
2. addWordToUserHistory returns early for blacklisted words, so a
   blacklisted word is never re-learned.
3. The per-commit un-blacklist only fires for words that are real in a
   read-only dictionary (main/contacts/apps) via the new
   DictionaryGroup.isInReadOnlyDict, so typing a real word restores it
   but committing a junk word does not.

Tests: new cases in DictionaryGroupTest cover gap 1 (history-only word
blacklisted on removal), the read-only regression (main-dict word still
blacklisted), and isInReadOnlyDict. Full offline unit suite: 218 tests,
the same 11 pre-existing failures as origin/main (parser/xlink/emoji and
3 known InputLogic), 0 new failures; the 3 new tests pass.

Note for reviewers: gaps 2 and 3 are in DictionaryFacilitatorImpl, which
needs the native dictionaries for an end-to-end test; they are
compile-verified + covered by reasoning here. A full integration test
belongs to the A3 trace/replay harness (#21).

Addresses #38 (part of #18).
From an independent cross-model review of the blocklist fix:

- isInReadOnlyDict -> isInNonHistoryDictionary: also treat the user's personal dictionary (TYPE_USER) as a known word, so a word the user deliberately added and then types un-blacklists on commit. The previous helper only covered read-only main/contacts/apps, which could trap a personal word as permanently blocked once blacklisted.

- addToPersonalDictionaryIfInvalidButInHistory now skips blacklisted words, so a deleted word cannot be auto-promoted back into the personal dict (belt-and-suspenders; removeWord already resets history frequency).

- Tests cover main-dict, personal-dict, junk, and a blacklisted main-dict word (lookup bypasses the blacklist).
@AsafMah

AsafMah commented Jun 5, 2026

Copy link
Copy Markdown
Owner Author

Pushed a follow-up (9f50dd4) from an independent cross-model review (dogfooding the new rubber-duck tool on gpt-5.5). The review confirmed the core fix is sound after I verified removeWord's only caller is the explicit long-press-drag-to-delete gesture (SuggestionStripView -> LatinIME.removeSuggestion), so blacklisting there is the intended "trash forever" - not ordinary editing. It also surfaced one real gap I fixed:

  • Personal-dict words could get trapped blocked. isInReadOnlyDict (main/contacts/apps only) is now isInNonHistoryDictionary, which also treats the user's personal dictionary (TYPE_USER) as "known" - so a word the user deliberately added and then types un-blacklists on commit. Only the auto-learned user-history dict is excluded (that's where junk lives).
  • Auto-promotion guard: addToPersonalDictionaryIfInvalidButInHistory now skips blacklisted words.
  • Tests extended: personal-dict word, and a blacklisted main-dict word (lookup bypasses the blacklist, so typing it can still restore it).

AsafMah added a commit that referenced this pull request Jun 5, 2026
… verify caveat)

From an independent cross-model review of the roadmap (rubber-duck on gpt-5.5):

- Grounding note now states claims are evidence-backed hypotheses and file:line refs are a snapshot; re-verify the code path before implementing.

- B6b: corrected 'small/Low-Med decouple' to Difficulty Med - ~8 InputLogicTest cases lock in the eager-space contract, so those behavior tests must be redesigned (not just updated) and the feel playtested (#23). Dropped B6b as the 'structurally-correct-first' example.

- C4a-d: marked as landed in PR #43; reflects isInNonHistoryDictionary (personal-dict words un-block too) rather than the stale 'read-only dict' framing.
@AsafMah

AsafMah commented Jun 5, 2026

Copy link
Copy Markdown
Owner Author

Manually verified on-device (Samsung Galaxy S24+, Hebrew + English) on a clean-data build: learn a junk word, long-press-drag it off the suggestion strip, then re-type/swipe -> it stays gone (the core fix); a real word deleted the same way returns on re-typing. With the unit tests (218/11, 0 new) and the cross-model review, merging.

@AsafMah
AsafMah merged commit 690dfdc into main Jun 5, 2026
1 check passed
AsafMah added a commit that referenced this pull request Jun 5, 2026
* docs: add keyboard & two-thumb improvement plan (living roadmap)

Captures the full design discussion behind the roadmap epics/issues (project: Two-Thumb & Keyboard Roadmap). Referenced by the seeded issues #13-#42.

* docs: temper roadmap claims after dogfood review (B6b reality, C4 #43, verify caveat)

From an independent cross-model review of the roadmap (rubber-duck on gpt-5.5):

- Grounding note now states claims are evidence-backed hypotheses and file:line refs are a snapshot; re-verify the code path before implementing.

- B6b: corrected 'small/Low-Med decouple' to Difficulty Med - ~8 InputLogicTest cases lock in the eager-space contract, so those behavior tests must be redesigned (not just updated) and the feel playtested (#23). Dropped B6b as the 'structurally-correct-first' example.

- C4a-d: marked as landed in PR #43; reflects isInNonHistoryDictionary (personal-dict words un-block too) rather than the stale 'read-only dict' framing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C4-blocklist: Fix blocklist gaps (the learned-typo cure)

1 participant