fix(dictionary): stop deleted/junk words from resurrecting (incl. via swipe)#43
Conversation
… 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).
|
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
|
… 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.
|
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. |
* 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.
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 * weightwherelanguageDistancefalls 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:removeWordonly 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.addWordToUserHistorynever checked the blacklist before learning.Fix
removeWordnow blacklists a word that lived only in the mutable user-history/personal dict. The existing suggestion filter then hides it (incl. the glide path).addWordToUserHistoryearly-returns for blacklisted words.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)
DictionaryGroupTestcases: history-only word blacklisted on removal (gap 1); main-dict word still blacklisted (read-only regression);isInReadOnlyDictbehavior.:app:testOfflineDebugUnitTest: 218 tests, 11 failed vs baselineorigin/main215 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
DictionaryFacilitatorImplwhich 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).