Repository navigation
F3: English prediction + Tab completion, SCOWL lexicon, learning from typed words - #3
Merged
Merged
Conversation
Adds LatinLexicon::complete() (personal-word-count > tech-seed > SCOWL-frequency-tier > alphabetical) and wires it into KeyHandler as a completion tooltip on a locked Latin run plus Tab (accept top-1) and Shift+Tab (candidate window, re-queries as you keep typing). Tab's existing P1 boundary-commit behavior is skipped only when a completion is actually available, so "no completion" still matches unchanged P1 semantics exactly. latin-words.txt now carries a SCOWL/ESDB size-tier rank (license verified: MIT-like for a generated word list <=80, see ACKNOWLEDGEMENTS.md) for ~39% of the dictionary; tech-seed keeps priority over it via a load-order rank offset in loadBuiltinWordList(). latin-user.txt gains a use-count column (word\tcount), backward compatible with plain P1-era lines. Updates MixedScriptKeyHandlerTests' R5 test to scope itself to latinCompletionEnabled=false, since Tab completing a rule-A run is now the intended P3 behavior for that exact scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eval, isolation)
1. Rebuild Source/Data/latin-words.txt entirely from SCOWL/ESDB (size<=70,
American spelling, variant level <=1) instead of macOS's 1934 web2 word
list with SCOWL only as a frequency-tier annotation. web2 had no
inflected forms at all; the new list carries them directly and every
word now has a rank tier (was ~39%). tools/lexicon/build_lexicon.py
rewritten to build both the word set and its tiers from a local SCOWL
repo checkout in one pass; tools/lexicon/scowl-size-tiers.tsv (the old
annotation-only file) is removed. 141,697 words (was 204,217): 80,067
added, 142,587 removed, 61,630 kept -- ACKNOWLEDGEMENTS.md and
README.markdown updated to match. P1 regression: F1 app-path retention
holds at 349/385 = 90.6%, pure-Chinese ON=OFF still identical character
for character; a sweep of every tone-1 single-syllable reading in
data.txt found exactly 2 (of 138 with a key sequence >= 3 letters)
changed Rule-B ambiguity status ("ful"/ㄑㄧㄠ newly ambiguous,
"tji"/ㄔㄨㄛ no longer is) -- neither is used by any existing test, and
Rule B never overrides the default Chinese candidate on its own.
2. Learn from Latin runs the user actually types and commits (Enter/
space/punctuation), not only from an explicit Tab/candidate-window
completion accept -- new Preferences.latinLearnTypedWords (default
on). KeyHandler's _commitMixedScriptLatinRun: gains a mayLearn flag so
Tab landing there (no completion was on offer) never counts as
evidence the user meant to finish the word, and
MixedScriptTracker::latinRunAlreadyRemembered() prevents double-
counting a run a completion accept already wrote. Esc/Backspace never
reach the commit path, so a canceled run is never learned.
3. Tab (and the completion tooltip/Shift+Tab candidate window) must not
grow an already-finished word into a longer dictionary entry sharing
its prefix -- new KeyHandler _isAlreadyCompleteWord:lexicon: gate
(word is itself a known word whose rank is at least as good as the
best longer completion), applied at the pending-run, committed-node,
and tooltip call sites. "acer" (tech-seed) stays "acer", not
"acerbic"; "the" stays "the". Rule-B alternates are exempt so e.g.
"coo" can still complete into "cook" on a second Tab.
testR5_TabOnARuleARunDoesNotWriteTheUserLexicon restored to its
original semantics; testTabDoesNotExtendACompleteWord added.
4. LatinCompletionKeyHandlerTests' eval now classifies "never completable"
tokens into proper-noun/abbreviation, inflected-form, and other, and
adds a "with history" pass that replays eval200 in corpus order,
teaching each row's eligible words (mirroring fix #2) before scoring
the next row. Inflected-form count is 0 in both passes, confirming
fix #1's dictionary switch. BASELINE.md's P3 section replaced with
both tables.
5. KeyHandlerBopomofoTests (upstream, mixedScript-unaware) now forces
mixedScriptEnabled/latinCompletionEnabled/latinLearnTypedWords off in
setUp and restores the real values in tearDown, so it no longer
inherits whatever this machine's live plist happens to have (it was
failing testInvalidBpmf whenever MixedScriptEnabled=1 on disk). Full
suite verified green with the real pref both true and false, and the
real plist ends each run unchanged.
6. New testing-only LanguageModelManager.resetLatinLexiconForTesting()
(and LatinLexicon::reset()) reloads the process-wide Latin lexicon
fresh before every KeyHandler-level test, fixing the cross-test
pollution from REVERIFY-P1-2026-09-10.md's R12 (an accepted word in
one test used to silently change another test's top completion).
Waits for any load already in flight before resetting to avoid a
data race with the test host app's own startup load. Word-choice
workaround comments from the pollution era removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Addresses docs/REVIEW-P3-2026-09-11.md's B1-B5 plus the non-blocking
items that shared a root cause with them.
B1 complete()'s top-n heap kept the wrong end of itself. std::push_heap
with comparator `comp` puts the comp-maximum at front(), so passing the
*reversed* "worse" relation made front() the best candidate and popped it
on every improvement -- everything scanned after the heap filled was
discarded. n=1 was unaffected, which is why the tooltip, the gate and the
whole eval never saw it and only the candidate window was visibly wrong
("th" offered thad/thaddeus/thai/thailand). Now uses `better` directly,
with a property test that checks complete(prefix, n) against a full sort
for every prefix and every n.
B2 learn-from-typing wrote any committed 3+ letter run straight to
latin-user.txt, where it outranked the entire dictionary forever. A
committed run is now only a candidate: 3-20 lowercase ASCII; a word the
lexicon already knows is recorded at once; anything else is staged in
memory and reaches disk only after a second, separate commit. A locked
Rule-A run keeps absorbing letters until a non-letter key, so a run with
Chinese keys glued onto it cannot be told from a new word when seen once
-- "twice" is the only signal available. Ordering follows: only a
confirmed user word (score >= 2) beats the dictionary, and an explicit
Tab/candidate accept carries that weight on its own.
B3 the "already a finished word" gate compared raw rank(), and every
hand-ranked seed term outranks every dictionary word, so Tab rewrote
code -> codesign, test -> testflight, and "run" permanently suggested
"runway". Adds LatinLexicon::sourceTier(), which is comparable across
files and ties the seed list with the dictionary's most frequent tier,
plus a three-character floor so "pr"/"th" stay prefixes in progress.
B4 Shift+letter is upstream's force-English gesture and bypasses every
mixedScript path, so a pending Rule-A run was cleared away with
everything else: "the" + Shift+A lost "the". It now commits the pending
run as an ordinary boundary first.
B5 with a pending run on screen and nothing for Tab to do, handleInput
returned NO and the host application got the Tab. It is consumed and
beeps instead.
Also: test preferences no longer leak into the real defaults domain
(PreferenceSandbox + tools/eval/check_plist_unchanged.sh, N4); the eval's
never-completable split asks the dictionary before it looks at casing,
which moves 61 tokens from "missing word" to "ranking miss" and shows 62%
of the gap is ranking, not vocabulary (N2); turning completion off now
stops the disk writes too (N3); the learn policy has behaviour tests,
killing both surviving mutants (N5); the reset spin has a deadline (N14);
parallel testing is root-caused (one defaults domain shared by every
runner process, not the lexicon) and documented as unsupported (N15); and
the SCOWL expansion and copyright notice are corrected (N16).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
testEval200LatinCompletion rewrites the whole P3 section of BASELINE.md, so editing the prose in the file itself was undone by the next test run. Moves the N2 correction (dictionary lookup before casing; 62% of the gap is ranking, not vocabulary) into the report string that produces it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/REVERIFY-P3-2026-09-12.md closed B1-B5 with 0/6 mutation survivors but found four problems of its own. This is all four plus the two items it wanted fixed before the input method is actually installed. P-1 (data loss). persistUserWords() truncated the target file and dumped userWords_ over it, so pointing the user-phrase folder at one that already had a latin-user.txt -- or having Dropbox add words to the one in use -- deleted them. It now reads the target, merges per word (larger count wins) and lands through a temp file plus rename. The other half is that nothing reloaded the Latin lexicon when the folder moved: LatinLexicon::reloadUserWordList() plus LanguageModelManager's +reloadLatinUserWordList, called from AppDelegate.updateUserPhrases() next to the Chinese user phrases it has always reloaded. B2 staging moved from memory to disk. Requiring two commits of an unknown word before learning it was right, but the first sighting lived in a process-lifetime-only map, so a logout or an input method switch reset the count -- and the words that need learning (names, products, internal codenames) are typed twice a day, hours apart, never twice in one process. The first sighting is written at score 1 now, and complete() omits an unconfirmed word the dictionary does not know entirely rather than ranking it last, so "recorded" and "suggested" stay different things. Prediction tooltip floor 2 -> 4 letters. The reverify's dogfood profile: over 100 keystrokes of 17 everyday words, a floor of 2 showed the wrong word 33 times to save 7 keystrokes. At 4 that is 11 wrong frames for 6 saved keystrokes. Tab and Shift+Tab are unchanged and still complete a two-letter run on demand. P-2/P-3 (test isolation). main.swift skips populateDefaults() under XCTest, since the app bundle is the test host and those 23 writes landed before PreferenceSandbox could snapshot the domain. Tests redirect their user-data folder through LanguageModelManager.dataFolderOverrideForTesting instead of the CustomUserPhraseLocation preference every process on the machine shares -- that key is how a parallel run put 130 eval-corpus words in the developer's real latin-user.txt. check_plist_unchanged.sh now guards ~/Library/Application Support/McBopomofo/ as well as the plist, which is what would have caught it. P-4 (eval honesty). The "with history" pass replays learning by typing and committing each token, the only thing production does, instead of acceptLatinCompletion() which confirms a word in one call; and tokens Rule A never locked get their own category instead of being counted as ranking misses. Both tables re-run: ranking misses in the no-history pass are 81, the number the reverify computed by hand, not 96. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P3 of the plan, four rounds (implementation → six fixes → opus review (5 blocking) → fixes → opus re-verify (2 blocking) → fixes).
awesome ⇥); Tab accepts it and Shift+Tab opens a candidate window with the top-N — both usable from 2 letters, the tooltip just does not advertise earlier (a 100-keystroke probe showed 33 wrong frames at ≥2 vs 11 at ≥4). A run that is already a complete word (≥ 3 letters) from a source at least as strong as its best completion is left alone:code/test/run/the/acer+ Tab stay as typed.latinLearnTypedWords, default on): every committed rule-A run of 3–20 letters is written tolatin-user.txtwith score 1; a word only becomes a suggestion at score 2 (typed twice, or accepted once via Tab which counts double), so typos and runs glued to Bopomofo keys never surface. Cancelled runs, Shift-typed words and anything Chinese are never written. The file is read-merge-written atomically and reloaded when the user-phrase folder changes, so moving the folder (Dropbox) neither loses nor carries words.ACKNOWLEDGEMENTS.md.tools/eval/BASELINE.md§P3, 330 English tokens through the realKeyHandler, production learning rule): completable within 4 letters 23.0% (28.8% learning across the corpus in order), average 0.39–0.46 keystrokes saved per token. Never completable is split honestly: 81 ranking misses (SCOWL's five tiers leave no signal after the seed list — a real frequency source is the P4 lever), 55 not in the dictionary, 18 never locked as Latin by rule A, 32 below the tooltip floor but completable on demand.~/Library/Application Support/McBopomofo/(populateDefaultsskipped under XCTest, data folder injected in tests,PreferenceSandbox,tools/eval/check_plist_unchanged.shguards both). Parallel test execution unsupported (sharedUserDefaults.standard), documented inAGENTS.md.Reviews:
docs/REVIEW-P3-2026-09-11.md,docs/REVERIFY-P3-2026-09-12.md; all blocking items closed. Known non-blocking: candidate rows past the top 3 are alphabetical for common prefixes (data), manual edits tolatin-user.txtneed an IME restart, Rule-B alternates are not gated by the finished-word rule (deliberate).🤖 Generated with Claude Code