Skip to content

F3: English prediction + Tab completion, SCOWL lexicon, learning from typed words - #3

Merged
lmanchu merged 5 commits into
masterfrom
f3-latin-completion
Sep 12, 2026
Merged

lmanchu merged 5 commits into
masterfrom
f3-latin-completion

Conversation

@lmanchu

@lmanchu lmanchu commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

P3 of the plan, four rounds (implementation → six fixes → opus review (5 blocking) → fixes → opus re-verify (2 blocking) → fixes).

  • Prediction: once a Latin run reaches 4 letters the composing buffer's tooltip shows the top-1 completion (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.
  • Ranking: confirmed user words (score ≥ 2) > tech seed = SCOWL tier 0 > tiers 1–4 > alphabetical. Real top-n heap (round 1's comparator was inverted; only top-1 paths masked it).
  • Learning (latinLearnTypedWords, default on): every committed rule-A run of 3–20 letters is written to latin-user.txt with 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.
  • Lexicon: SCOWL/ESDB size ≤ 70, 141,697 words with size-bucket tiers (replaces Webster 1934); MIT-like licence quoted in ACKNOWLEDGEMENTS.md.
  • Eval (tools/eval/BASELINE.md §P3, 330 English tokens through the real KeyHandler, 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.
  • P1 regression: English retention 349/385 unchanged; pure-Chinese ON = OFF byte-identical.
  • Tests: 207 engine, 203 XCTest. The suite no longer touches the live preferences domain or ~/Library/Application Support/McBopomofo/ (populateDefaults skipped under XCTest, data folder injected in tests, PreferenceSandbox, tools/eval/check_plist_unchanged.sh guards both). Parallel test execution unsupported (shared UserDefaults.standard), documented in AGENTS.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 to latin-user.txt need an IME restart, Rule-B alternates are not gated by the finished-word rule (deliberate).

🤖 Generated with Claude Code

lmanchu and others added 5 commits September 11, 2026 19:59
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>
@lmanchu
lmanchu merged commit c8220a9 into master Sep 12, 2026
1 of 10 checks passed
@lmanchu
lmanchu deleted the f3-latin-completion branch September 12, 2026 04:35
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.

1 participant