From a53d43db43046de79c2c25e8f1e9c93301d042a2 Mon Sep 17 00:00:00 2001 From: lmanchu Date: Sun, 13 Sep 2026 20:10:18 +0800 Subject: [PATCH 1/2] chore(privacy): scrub maintainer-local traces so the repo can go public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything here is a private trace, not a behavior change: the maintainer's name, local absolute paths, the out-of-repo private corpus location, and a planning document that only ever existed on one machine. - Person: "Lman" -> "the maintainer" / 「維護者」 in comments, .gitignore, tools/eval docs, and the review reports. Copyright lines keep the real name; so do the lmanchu GitHub URLs and the bundle id. - Paths: /Users/lman/... -> ~/... or a description; a scratch-directory script reference loses its path. - Design notes: ~/.claude/plans/zhuyin-ime-personal.md (and its bare filename) never existed in this repo, so every reference now reads "the design notes". The README sentence citing it is gone. - Eval corpus: the two XCTest eval measurements read BOPOMIX_EVAL_CORPUS and XCTSkip when it is unset or missing, instead of hardcoding ~/Dev/mixime-private/eval200.tsv. check_plist_unchanged.sh forwards the variable as TEST_RUNNER_BOPOMIX_EVAL_CORPUS, which is the only spelling xcodebuild passes to the test host. build_corpus.py defaults to the gitignored eval-corpus/ directory; .gitignore's comment no longer names a private repo path. docs/REVIEW-* and docs/REVERIFY-* keep their content and conclusions verbatim -- only the private details inside them changed. Verified: xcodebuild build succeeds; the suite is 255 tests green under check_plist_unchanged.sh both with and without BOPOMIX_EVAL_CORPUS (2 skipped without it), PLIST_UNCHANGED and DATA_FOLDER_UNCHANGED each time, and BASELINE.md's numbers regenerate identically apart from latency. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 10 +++-- BopomixTests/KeyHandlerBopomofoTests.swift | 4 +- .../LatinCompletionKeyHandlerTests.swift | 27 ++++++++---- BopomixTests/MixedScriptKeyHandlerTests.swift | 20 ++++++--- README.markdown | 4 +- Source/Data/latin-tech-seed.txt | 2 +- Source/Engine/McBopomofoLM.h | 2 +- Source/Engine/MixedScript/CMakeLists.txt | 2 +- .../MixedScript/bopomofo_shape_tracker.h | 2 +- .../bopomofo_shape_tracker_test.cpp | 2 +- Source/Engine/MixedScript/latin_lexicon.h | 8 ++-- .../Engine/MixedScript/latin_passthrough_lm.h | 2 +- .../Engine/MixedScript/mixed_script_tracker.h | 6 +-- .../MixedScript/mixed_script_tracker_test.cpp | 2 +- ...ntroller+CandidateControllerDelegate.swift | 2 +- Source/KeyHandler.h | 2 +- Source/KeyHandler.mm | 24 +++++------ Source/LanguageModelManager+Privates.h | 2 +- Source/LanguageModelManager.mm | 4 +- Source/Preferences.swift | 10 ++--- docs/REVERIFY-P1-2026-09-10.md | 2 +- docs/REVERIFY-P3-2026-09-12.md | 6 +-- docs/REVIEW-P3-2026-09-11.md | 8 ++-- tools/eval/BASELINE.md | 11 ++--- tools/eval/README.md | 42 +++++++++++++------ tools/eval/bopomix_eval.cpp | 4 +- tools/eval/build_corpus.py | 22 ++++++---- tools/eval/check_plist_unchanged.sh | 9 ++++ tools/eval/run_eval.py | 8 ++-- tools/lexicon/build_lexicon.py | 2 +- 30 files changed, 151 insertions(+), 100 deletions(-) diff --git a/.gitignore b/.gitignore index 49d6395..814fcff 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,10 @@ codecov_comment.md DerivedData build-engine -# P0.5 eval harness: Lman's own text must never enter this repo. Any file -# matching *.private.* (corpus candidates, generated eval sets derived from -# personal vault/Slack text) stays under ~/Dev/mixime-private/ only. +# P0.5 eval harness: nobody's personal text may enter this repo. Corpus +# candidates and the eval TSVs generated from them are derived from private +# notes/chat logs, so keep them outside the repo (point BOPOMIX_EVAL_CORPUS +# at them) or under the ignored eval-corpus/ directory. # See tools/eval/README.md. -*.private.* \ No newline at end of file +*.private.* +eval-corpus/ \ No newline at end of file diff --git a/BopomixTests/KeyHandlerBopomofoTests.swift b/BopomixTests/KeyHandlerBopomofoTests.swift index 55ab005..d417d1c 100644 --- a/BopomixTests/KeyHandlerBopomofoTests.swift +++ b/BopomixTests/KeyHandlerBopomofoTests.swift @@ -35,10 +35,10 @@ class KeyHandlerBopomofoTests: XCTestCase { var handler = KeyHandler() // P3 fix #5 (XCTest/machine-preference isolation, see - // ~/.claude/plans/zhuyin-ime-personal.md's P3 fix #5): these upstream + // the design notes' P3 fix #5): these upstream // tests know nothing about mixedScript and never save/restore its // preferences themselves, so on a machine that already has - // MixedScriptEnabled=1 in its real plist (Lman dogfoods a master + // MixedScriptEnabled=1 in its real plist (the maintainer dogfoods a master // build) they would otherwise inherit that value and see mixedScript // rewrite readings it never used to (e.g. testInvalidBpmf's "ni4" // becomes a Rule-A literal run instead of the upstream diff --git a/BopomixTests/LatinCompletionKeyHandlerTests.swift b/BopomixTests/LatinCompletionKeyHandlerTests.swift index 3ac1c08..95949e5 100644 --- a/BopomixTests/LatinCompletionKeyHandlerTests.swift +++ b/BopomixTests/LatinCompletionKeyHandlerTests.swift @@ -26,7 +26,7 @@ import XCTest @testable import Bopomix /// KeyHandler-level integration tests for P3 English prediction + Tab -/// completion (see ~/.claude/plans/zhuyin-ime-personal.md's F3 scope). +/// completion (see the design notes' F3 scope). /// Mirrors MixedScriptKeyHandlerTests' approach (drive the real KeyHandler /// FSM, not the engine in isolation) for the same reason P1's review found: /// every interesting interaction here is with candidate-window/Tab/grid @@ -353,7 +353,7 @@ class LatinCompletionKeyHandlerTests: XCTestCase { XCTAssertNil(predictedCompletion, "\(state)") } - /// P3 fix #3 (see ~/.claude/plans/zhuyin-ime-personal.md's P3 fix #3 + /// P3 fix #3 (see the design notes' P3 fix #3 /// and KeyHandler's _offeredCompletionFor:lexicon:): once a pending run is /// itself already a recognized word ranked at least as well as the /// best longer completion sharing its prefix, that counts as "the @@ -1503,9 +1503,11 @@ class LatinCompletionKeyHandlerTests: XCTestCase { } /// How many letters of an English token does the user actually have - /// to type before Tab would complete it, on eval200 (the only - /// Traditional-Mandarin-plus-real-project-vocabulary corpus this repo - /// has) -- measured two ways (P3 fix #4): "no history" evaluates + /// to type before Tab would complete it, on the corpus named by + /// `BOPOMIX_EVAL_CORPUS` (a Traditional-Mandarin-plus-real-project- + /// vocabulary TSV, kept outside this repo -- build one from your own + /// text with tools/eval/build_corpus.py) -- measured two ways + /// (P3 fix #4): "no history" evaluates /// every token cold, against just the built-in dictionary/tech seed; /// "with history" replays the corpus in row order, and after /// evaluating each row's tokens, types and commits every eligible one @@ -1514,14 +1516,23 @@ class LatinCompletionKeyHandlerTests: XCTestCase { /// enough to be confirmed becomes more completable later in the /// corpus. This is a measurement/report, not a /// correctness gate (except the pure-Chinese control below): skipped - /// outright when the private corpus is not present, and neither table + /// outright when `BOPOMIX_EVAL_CORPUS` is unset, and neither table /// asserts thresholds the way testEval200ThroughKeyHandler does -- /// there is no prior baseline to hold either to yet, this run creates /// one for both. func testEval200LatinCompletion() throws { - let corpusPath = NSHomeDirectory() + "/Dev/mixime-private/eval200.tsv" + guard let corpusPath = ProcessInfo.processInfo.environment["BOPOMIX_EVAL_CORPUS"], + !corpusPath.isEmpty + else { + throw XCTSkip( + "BOPOMIX_EVAL_CORPUS is not set; nothing to measure. Point it at a " + + "corpus TSV built from your own text with tools/eval/build_corpus.py.") + } guard FileManager.default.fileExists(atPath: corpusPath) else { - throw XCTSkip("corpus not present at \(corpusPath); nothing to measure") + throw XCTSkip( + "BOPOMIX_EVAL_CORPUS points at \(corpusPath), which does not exist; " + + "nothing to measure. Build a corpus TSV from your own text with " + + "tools/eval/build_corpus.py.") } let rows = try loadEvalRows(at: corpusPath) XCTAssertFalse(rows.isEmpty) diff --git a/BopomixTests/MixedScriptKeyHandlerTests.swift b/BopomixTests/MixedScriptKeyHandlerTests.swift index 16281d6..3bb7ea2 100644 --- a/BopomixTests/MixedScriptKeyHandlerTests.swift +++ b/BopomixTests/MixedScriptKeyHandlerTests.swift @@ -26,7 +26,7 @@ import XCTest @testable import Bopomix /// KeyHandler-level integration tests for P1 zh/en mixed typing (see -/// ~/.claude/plans/zhuyin-ime-personal.md). The engine-level tests under +/// the design notes). The engine-level tests under /// `Source/Engine/MixedScript/` cover the rules in isolation; everything /// here drives the real `KeyHandler` FSM the way the input method does, /// because every blocking defect found in the first review round @@ -791,10 +791,18 @@ class MixedScriptKeyHandlerTests: XCTestCase { /// dropping a whole run). This types the same corpus through the real /// `KeyHandler` instead and is the number that counts. func testEval200ThroughKeyHandler() throws { - let corpusPath = - NSHomeDirectory() + "/Dev/mixime-private/eval200.tsv" + guard let corpusPath = ProcessInfo.processInfo.environment["BOPOMIX_EVAL_CORPUS"], + !corpusPath.isEmpty + else { + throw XCTSkip( + "BOPOMIX_EVAL_CORPUS is not set; nothing to measure. Point it at a " + + "corpus TSV built from your own text with tools/eval/build_corpus.py.") + } guard FileManager.default.fileExists(atPath: corpusPath) else { - throw XCTSkip("corpus not present at \(corpusPath); nothing to measure") + throw XCTSkip( + "BOPOMIX_EVAL_CORPUS points at \(corpusPath), which does not exist; " + + "nothing to measure. Build a corpus TSV from your own text with " + + "tools/eval/build_corpus.py.") } let rows = try loadCorpus(at: corpusPath) XCTAssertFalse(rows.isEmpty) @@ -946,7 +954,7 @@ class MixedScriptKeyHandlerTests: XCTestCase { /// R5: a rule-A run has a single candidate. Tab-cycling over it is not a /// choice between Chinese and English and must not touch latin-user.txt. /// - /// P3 (see ~/.claude/plans/zhuyin-ime-personal.md's F3 scope) gives Tab + /// P3 (see the design notes' F3 scope) gives Tab /// a new, deliberate job on a pending rule-A run -- completing it into /// a longer dictionary word -- but P3 fix #3 also gives it a deliberate /// *non*-job here: "acer" is tech-seed ranked ahead of every longer @@ -973,7 +981,7 @@ class MixedScriptKeyHandlerTests: XCTestCase { XCTAssertEqual(after, before, "Tab over a rule-A run must not remember the word") } - /// P3 fix #3 (see ~/.claude/plans/zhuyin-ime-personal.md's P3 fix #3 + /// P3 fix #3 (see the design notes' P3 fix #3 /// and KeyHandler's _offeredCompletionFor:lexicon:): Tab must not silently /// grow an already-finished word into a longer dictionary entry just /// because one happens to share its prefix. Companion to diff --git a/README.markdown b/README.markdown index ee6fc5d..3db71da 100644 --- a/README.markdown +++ b/README.markdown @@ -2,9 +2,7 @@ ## mixime:中英混打(P1,預設關閉) -這個 fork 多了「不切換輸入法直接打英文」的功能(見 -`~/.claude/plans/zhuyin-ime-personal.md` 的 F1/P1 節)。P1 期間**預設關閉**, -要 dogfood 請自己打開: +P1 期間**預設關閉**,要 dogfood 請自己打開: ```sh defaults write io.github.lmanchu.inputmethod.bopomix MixedScriptEnabled -bool true diff --git a/Source/Data/latin-tech-seed.txt b/Source/Data/latin-tech-seed.txt index db6faa8..891ce39 100644 --- a/Source/Data/latin-tech-seed.txt +++ b/Source/Data/latin-tech-seed.txt @@ -1,5 +1,5 @@ # Hand-curated tech/product term seed list for the P1 zh/en mixed-typing -# feature (see ~/.claude/plans/zhuyin-ime-personal.md's "P1 設計" section). +# feature (see the design notes' "P1 設計" section). # Loaded on top of latin-words.txt (see tools/lexicon/build_lexicon.py) so # these terms are guaranteed present even though many are not real English # dictionary words and would never appear in a system word list. Rank diff --git a/Source/Engine/McBopomofoLM.h b/Source/Engine/McBopomofoLM.h index 7968eaf..43413df 100644 --- a/Source/Engine/McBopomofoLM.h +++ b/Source/Engine/McBopomofoLM.h @@ -111,7 +111,7 @@ class McBopomofoLM : public Formosa::Gramambular2::LanguageModel { std::function macroConverter); std::string convertMacro(const std::string& input) const; - // P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md). + // P1 zh/en mixed typing (see the design notes). // When disabled (the default), getUnigrams()/hasUnigrams() ignore // mixedScriptLM() entirely, so mixedScriptEnabled=false leaves existing // behavior byte-for-byte unchanged. KeyHandler drives mixedScriptLM() diff --git a/Source/Engine/MixedScript/CMakeLists.txt b/Source/Engine/MixedScript/CMakeLists.txt index 779ee23..32e5e1e 100644 --- a/Source/Engine/MixedScript/CMakeLists.txt +++ b/Source/Engine/MixedScript/CMakeLists.txt @@ -4,7 +4,7 @@ project(MixedScript) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") -# P1 zh/en mixed-typing (see ~/.claude/plans/zhuyin-ime-personal.md). Mirrors +# P1 zh/en mixed-typing (see the design notes). Mirrors # the Mandarin/ and gramambular2/ subdirectory pattern: a small static lib # plus its own gtest binary, consumed by McBopomofoLMLib (McBopomofoLM # merges LatinPassthroughLM into its unigram dispatch) and by KeyHandler.mm diff --git a/Source/Engine/MixedScript/bopomofo_shape_tracker.h b/Source/Engine/MixedScript/bopomofo_shape_tracker.h index 443b2cd..b86aa2d 100644 --- a/Source/Engine/MixedScript/bopomofo_shape_tracker.h +++ b/Source/Engine/MixedScript/bopomofo_shape_tracker.h @@ -51,7 +51,7 @@ namespace McBopomofo::MixedScript { // (there is only one consonant/medial/vowel slot per syllable, and a tone // key -- which is exempt from this check -- always ends the syllable and // is composed immediately by KeyHandler, which resets this tracker before -// the next key). See zhuyin-ime-personal.md's P1 design section for the +// the next key). See the design notes' P1 design section for the // quantification this mirrors (~74% of English tokens become // "uncomposable" within 2-3 letters) and the explicit "th"/"sl" examples. // diff --git a/Source/Engine/MixedScript/bopomofo_shape_tracker_test.cpp b/Source/Engine/MixedScript/bopomofo_shape_tracker_test.cpp index cd85ee8..941e6c2 100644 --- a/Source/Engine/MixedScript/bopomofo_shape_tracker_test.cpp +++ b/Source/Engine/MixedScript/bopomofo_shape_tracker_test.cpp @@ -67,7 +67,7 @@ TEST(BopomofoShapeTrackerTest, ToneKeyNeverConflicts) { // "th": t -> CH (consonant), h -> C (consonant). Two different consonants // in a row is the required early-reject example from -// zhuyin-ime-personal.md's P1 design section. +// the design notes' P1 design section. TEST(BopomofoShapeTrackerTest, ThRejectsAtSecondLetter) { BopomofoShapeTracker tracker; EXPECT_TRUE(tracker.feed(Standard(), 't')); diff --git a/Source/Engine/MixedScript/latin_lexicon.h b/Source/Engine/MixedScript/latin_lexicon.h index 453a4b2..5bf36cc 100644 --- a/Source/Engine/MixedScript/latin_lexicon.h +++ b/Source/Engine/MixedScript/latin_lexicon.h @@ -25,7 +25,7 @@ // MixedScriptTracker (see mixed_script_tracker.h) to tell whether a run of // ASCII letters typed without leaving Bopomofo mode is more likely an // English word than a Chinese reading. See -// ~/.claude/plans/zhuyin-ime-personal.md's "P1 設計" section for the +// the design notes' "P1 設計" section for the // product rationale (rules A/B/C). #ifndef SRC_ENGINE_MIXEDSCRIPT_LATIN_LEXICON_H_ @@ -141,7 +141,7 @@ class LatinLexicon { // trailing space: the built-in list is far too permissive for that (a // two-letter run like "up"/"el" is both an English word and an extremely // common tone-1 syllable), whereas a word in this store is one the user - // has personally disambiguated before. See zhuyin-ime-personal.md's P1 + // has personally disambiguated before. See the design notes' P1 // section and docs/REVIEW-P1-2026-09-10.md's B1/B2. bool isUserWord(const std::string& text) const; @@ -156,7 +156,7 @@ class LatinLexicon { // sorted lazily on first use, which used to cost ~294 ms on the key // thread the first time anything called this. Not on P1's hot path // (MixedScriptTracker's decision logic never calls it); kept for P3's - // predictive-typing work, see zhuyin-ime-personal.md's F3 scope. + // predictive-typing work, see the design notes' F3 scope. bool isPrefix(const std::string& text) const; // Returns the word's rank (0 = most frequent) or -1 if `text` is not a @@ -196,7 +196,7 @@ class LatinLexicon { // "th" completing normally. int sourceTier(const std::string& text) const; - // P3 predictive typing (see zhuyin-ime-personal.md's F3 scope). Returns + // P3 predictive typing (see the design notes' F3 scope). Returns // up to `n` known words that start with (but are longer than) `prefix`, // best completion first. Ordering: a *confirmed* user word (score >= // kUserWordConfirmedScore) beats every builtin word, ranked among diff --git a/Source/Engine/MixedScript/latin_passthrough_lm.h b/Source/Engine/MixedScript/latin_passthrough_lm.h index 439e58e..2a6d7f0 100644 --- a/Source/Engine/MixedScript/latin_passthrough_lm.h +++ b/Source/Engine/MixedScript/latin_passthrough_lm.h @@ -85,7 +85,7 @@ class LatinPassthroughLM : public Formosa::Gramambular2::LanguageModel { // window -- both funnel through fixNodeWithReading:) to tell whether the // user just explicitly picked a mixedScript Latin candidate, which is // when (and only when) LatinLexicon::rememberWord() should be called -- - // see zhuyin-ime-personal.md's P1 design notes on why the automatic + // see the design notes' P1 design notes on why the automatic // dictionary-plus-space default must NOT itself count as learning. bool hasValue(const std::string& key, const std::string& value) const; diff --git a/Source/Engine/MixedScript/mixed_script_tracker.h b/Source/Engine/MixedScript/mixed_script_tracker.h index 7a8de41..bcc6bf0 100644 --- a/Source/Engine/MixedScript/mixed_script_tracker.h +++ b/Source/Engine/MixedScript/mixed_script_tracker.h @@ -33,7 +33,7 @@ namespace McBopomofo::MixedScript { // The three-way call for one contiguous run of ASCII letters typed without -// leaving Bopomofo mode. See zhuyin-ime-personal.md's P1 design section +// leaving Bopomofo mode. See the design notes' P1 design section // for the product rationale. enum class Verdict { // No English signal (yet). Caller keeps feeding the real @@ -148,7 +148,7 @@ class MixedScriptTracker { // way -- this only touches mixedScript's own bookkeeping. void popLastLatinChar(); - // P3 predictive typing (see zhuyin-ime-personal.md's F3 scope and + // P3 predictive typing (see the design notes' F3 scope and // LatinLexicon::complete()): replaces the pending run's text with a // longer completion the user just accepted (Tab, or a pick from the // completion candidate window -- see KeyHandler's mixed-script hookup). @@ -166,7 +166,7 @@ class MixedScriptTracker { void acceptCompletion(const std::string& word); // P3 "learn from what you actually type" (see - // zhuyin-ime-personal.md's P3 fix #2): true if the run's *current* text + // the design notes' P3 fix #2): true if the run's *current* text // was already explicitly written to the user's lexicon by a completion // accept (acceptCompletion()) and has not changed since. KeyHandler's // _commitMixedScriptLatinRun uses this to avoid double-counting a word diff --git a/Source/Engine/MixedScript/mixed_script_tracker_test.cpp b/Source/Engine/MixedScript/mixed_script_tracker_test.cpp index 6f9714a..f13d5f3 100644 --- a/Source/Engine/MixedScript/mixed_script_tracker_test.cpp +++ b/Source/Engine/MixedScript/mixed_script_tracker_test.cpp @@ -320,7 +320,7 @@ TEST(MixedScriptTrackerTest, PopLastLatinCharAfterAcceptingCompletion) { EXPECT_EQ(tracker.latinRun(), "the"); } -// P3 fix #2 (see zhuyin-ime-personal.md's P3 fix #2 and +// P3 fix #2 (see the design notes' P3 fix #2 and // LatinLexicon::rememberWord()'s use-count doc): acceptCompletion() marks // the run as already remembered so KeyHandler's natural-typing learn hook // does not double-count it when the same run later reaches an ordinary diff --git a/Source/InputMethodController+CandidateControllerDelegate.swift b/Source/InputMethodController+CandidateControllerDelegate.swift index 9bf2678..773fba2 100644 --- a/Source/InputMethodController+CandidateControllerDelegate.swift +++ b/Source/InputMethodController+CandidateControllerDelegate.swift @@ -57,7 +57,7 @@ extension BopomixInputMethodController: CandidateControllerDelegate { let selectedCandidate = state.candidates[Int(index)] // P3 English prediction + Tab completion (see - // ~/.claude/plans/zhuyin-ime-personal.md's F3 scope): a pick + // the design notes' F3 scope): a pick // from the completion candidate window is not a grid // position fixNode(...) could override (see // KeyHandler.h's acceptLatinCompletionWithValue: doc), so it diff --git a/Source/KeyHandler.h b/Source/KeyHandler.h index 7f42f60..bff91ef 100644 --- a/Source/KeyHandler.h +++ b/Source/KeyHandler.h @@ -78,7 +78,7 @@ extern InputMode InputModePlainBopomofo; associatedPhraseValue:(NSString *)phraseValue; // P3 English prediction + Tab completion (see -// ~/.claude/plans/zhuyin-ime-personal.md's F3 scope): accepts `value` as +// the design notes' F3 scope): accepts `value` as // the pending Latin run's completion, the same way Tab does, but from a // pick in the completion candidate window (see // InputMethodController+CandidateControllerDelegate.swift's diff --git a/Source/KeyHandler.mm b/Source/KeyHandler.mm index de566d6..bc85868 100644 --- a/Source/KeyHandler.mm +++ b/Source/KeyHandler.mm @@ -51,7 +51,7 @@ InputMode InputModePlainBopomofo = @"io.github.lmanchu.inputmethod.bopomix.PlainBopomofo"; // P3 English prediction + Tab completion (see -// ~/.claude/plans/zhuyin-ime-personal.md's F3 scope). The synthetic +// the design notes' F3 scope). The synthetic // "reading" every candidate in the completion candidate window carries -- // never a real Bopomofo reading or grid position, so it doubles as this // window's marker: _isLatinCompletionCandidateState: and the Swift @@ -103,7 +103,7 @@ @implementation KeyHandler { NSString *_inputMode; - // P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md). + // P1 zh/en mixed typing (see the design notes). McBopomofo::MixedScript::MixedScriptTracker *_mixedScriptTracker; // The (reading, value) pairs this composition registered as rule-B @@ -133,7 +133,7 @@ - (void)setInputMode:(NSString *)value newLanguageModel = [LanguageModelManager languageModelPlainBopomofo]; newLanguageModel->setPhraseReplacementEnabled(false); // P1 zh/en mixed typing is scoped to InputModeBopomofo only (see - // zhuyin-ime-personal.md's P1 design section); Plain Bopomofo gets + // the design notes' P1 design section); Plain Bopomofo gets // no English-awareness in this pass. newLanguageModel->setMixedScriptEnabled(false); } else { @@ -565,7 +565,7 @@ - (BOOL)handleInput:(KeyHandlerInput *)input state:(InputState *)inState stateCa // see if it's valid BPMF reading bool isValidKey = _bpmfReadingBuffer->isValidKey((char)charCode); - // MARK: P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md) + // MARK: P1 zh/en mixed typing (see the design notes) // // Only lowercase ASCII letters are ever fed to the tracker. Shift+letter // already took the CapsLock/Shift-forces-English path above (:391-405) @@ -1162,7 +1162,7 @@ - (BOOL)handleInput:(KeyHandlerInput *)input state:(InputState *)inState stateCa return NO; } -// MARK: P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md) +// MARK: P1 zh/en mixed typing (see the design notes) // Commits the tracker's pending Latin run (rule A) into the grid as a // single literal-text node and resets the tracker. Callers are @@ -1201,7 +1201,7 @@ - (void)_commitMixedScriptLatinRun:(BOOL)mayLearn return; } - // P3 fix #2 (see ~/.claude/plans/zhuyin-ime-personal.md's P3 fix #2): + // P3 fix #2 (see the design notes' P3 fix #2): // this is the single chokepoint every Rule-A run's boundary commit // passes through, so it is also the right place to learn from what // the user actually typed, not just what they explicitly accepted a @@ -1251,7 +1251,7 @@ - (void)_rememberLatinWord:(const std::string&)word weight:(int)weight } } -// P3 fix #2 (see ~/.claude/plans/zhuyin-ime-personal.md's P3 fix #2): +// P3 fix #2 (see the design notes' P3 fix #2): // "learn from what you actually type", not only from an explicit // completion accept. Called only from _commitMixedScriptLatinRun, i.e. // only for a Rule-A run that reached an ordinary boundary commit -- Esc @@ -1358,7 +1358,7 @@ - (BOOL)_mixedScriptAvailable } // P3 English prediction + Tab completion (see -// ~/.claude/plans/zhuyin-ime-personal.md's F3 scope): a strict narrowing +// the design notes' F3 scope): a strict narrowing // of _mixedScriptAvailable, so turning mixedScript off (or an unsupported // keyboard layout) disables completion the same way it disables // everything else P1 added, with no separate gate to keep in sync. @@ -1386,7 +1386,7 @@ - (BOOL)_isFinishedWord:(const std::string&)word against:(const std::string&)top // completion, or an empty string when there is none or when `word` is // already a finished word. // -// "Finished" (P3 fix #3, see ~/.claude/plans/zhuyin-ime-personal.md) +// "Finished" (P3 fix #3, see the design notes) // means all three of: // // 1. at least kMinFinishedLatinWordLength characters. A one- or @@ -1459,7 +1459,7 @@ - (BOOL)_isMixedScriptAlternateWithReading:(NSString *)reading value:(NSString * } // MARK: P3 English prediction + Tab completion (see -// ~/.claude/plans/zhuyin-ime-personal.md's F3 scope) +// the design notes' F3 scope) // Builds up to Preferences.candidateKeys.length completions for the // pending Latin run as a candidate window, or nil if there are none to @@ -2366,7 +2366,7 @@ - (BOOL)_handleCandidateState:(InputState *)state VTCandidateController *gCurrentCandidateController = [self.delegate candidateControllerForKeyHandler:self]; // MARK: P3 Latin completion candidate window (see - // ~/.claude/plans/zhuyin-ime-personal.md's F3 scope). Not grid-backed + // the design notes' F3 scope). Not grid-backed // -- the pending run it lists completions for has not been committed // to _grid (see handleInput:'s Tab branch) -- so it needs its own, // narrower handling instead of this method's grid-cursor and @@ -3543,7 +3543,7 @@ - (InputStateInputting *)buildInputtingState ? @(_mixedScriptTracker->latinRun().c_str()) : @(_bpmfReadingBuffer->composedString().c_str()); - // P3 English prediction (see ~/.claude/plans/zhuyin-ime-personal.md's + // P3 English prediction (see the design notes' // F3 scope): show the top-1 completion as a tooltip, never touching // the composing buffer itself -- unlike the candidate window, this // must not pop up on every single English letter. complete() already diff --git a/Source/LanguageModelManager+Privates.h b/Source/LanguageModelManager+Privates.h index c4f7921..515cfb1 100644 --- a/Source/LanguageModelManager+Privates.h +++ b/Source/LanguageModelManager+Privates.h @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN @property (class, readonly, nonatomic) McBopomofo::McBopomofoLM *languageModelPlainBopomofo; @property (class, readonly, nonatomic) McBopomofo::UserOverrideModel *userOverrideModel; @property (class, readonly, nonatomic) McBopomofo::VariantAnnotator *variantAnnotator; -// P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md). +// P1 zh/en mixed typing (see the design notes). // The bundled latin-words.txt / latin-tech-seed.txt plus the user's own // latin-user.txt (same folder as McBopomofo's user phrases -- see // +latinUserWordListPath). The first access kicks off a background load diff --git a/Source/LanguageModelManager.mm b/Source/LanguageModelManager.mm index cf1d003..9677c8f 100644 --- a/Source/LanguageModelManager.mm +++ b/Source/LanguageModelManager.mm @@ -93,7 +93,7 @@ static void LTLoadVariantAnnotatorData() } } -// P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md). +// P1 zh/en mixed typing (see the design notes). // Loads the two bundled word lists plus the user's own learned-word file // into gLatinLexicon. Safe to call more than once; only the first call // does any work. This never touches gLanguageModelMcBopomofo's @@ -132,7 +132,7 @@ static void LTLoadMixedScriptLexicon() NSString *userPath = [LanguageModelManager latinUserWordListPath]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ - // P3 predictive typing (see zhuyin-ime-personal.md's F3 scope): + // P3 predictive typing (see the design notes' F3 scope): // tech-seed is loaded *before* the dictionary so its hand-ranked // terms always outrank every dictionary word regardless of the // dictionary's own SCOWL-tier rank -- see diff --git a/Source/Preferences.swift b/Source/Preferences.swift index b315ca2..910e820 100644 --- a/Source/Preferences.swift +++ b/Source/Preferences.swift @@ -40,17 +40,17 @@ private let kChineseConversionEnabledKey = "ChineseConversionEnabled" private let kHalfWidthPunctuationEnabledKey = "HalfWidthPunctuationEnable" private let kEscToCleanInputBufferKey = "EscToCleanInputBuffer" private let kKeepReadingUponCompositionError = "KeepReadingUponCompositionError" -// P1 zh/en mixed typing (see ~/.claude/plans/zhuyin-ime-personal.md). No UI +// P1 zh/en mixed typing (see the design notes). No UI // yet (planned for P4) -- these are UserDefaults-only for now, per // AGENTS.md's Preferences convention. private let kMixedScriptEnabledKey = "MixedScriptEnabled" private let kMixedScriptLatinOnSpaceForUserWordsKey = "MixedScriptLatinOnSpaceForUserWords" -// P3 English prediction + Tab completion (see zhuyin-ime-personal.md's F3 +// P3 English prediction + Tab completion (see the design notes' F3 // scope). Gated on kMixedScriptEnabledKey too (see KeyHandler's // _mixedScriptAvailable) -- this only decides whether completion runs on // top of an already-available mixedScript run. private let kLatinCompletionEnabledKey = "LatinCompletionEnabled" -// P3 fix #2 (see zhuyin-ime-personal.md's P3 fix #2): learn from Latin +// P3 fix #2 (see the design notes' P3 fix #2): learn from Latin // runs actually typed and committed, not only from an explicit Tab/ // candidate-window completion accept. private let kLatinLearnTypedWordsKey = "LatinLearnTypedWords" @@ -358,7 +358,7 @@ class Preferences: NSObject { @UserDefault(key: kKeepReadingUponCompositionError, defaultValue: false) @objc static var keepReadingUponCompositionError: Bool - // MARK: P1 zh/en mixed typing (see zhuyin-ime-personal.md) + // MARK: P1 zh/en mixed typing (see the design notes) /// Master switch for zh/en mixed typing. Opt-in for now (P1): /// `defaults write io.github.lmanchu.inputmethod.bopomix @@ -386,7 +386,7 @@ class Preferences: NSObject { @objc static var mixedScriptLatinOnSpaceForUserWords: Bool // MARK: P3 English prediction + Tab completion (see - // zhuyin-ime-personal.md's F3 scope) + // the design notes' F3 scope) /// Whether a Latin run shows a top-1 completion tooltip and Tab/ /// Shift+Tab accept/cycle it. Defaults to on (unlike diff --git a/docs/REVERIFY-P1-2026-09-10.md b/docs/REVERIFY-P1-2026-09-10.md index 2c72d7a..65fd23b 100644 --- a/docs/REVERIFY-P1-2026-09-10.md +++ b/docs/REVERIFY-P1-2026-09-10.md @@ -16,7 +16,7 @@ 本輪新挖出的問題,其中 1 條 blocking。** - **需修後 merge** — 唯一的 blocking 是一行 gate(`mixedScriptActive` 沒檢查鍵盤配置)。 -- **可裝 dogfood,條件:鍵盤配置=標準(大千)**。Lman 這台 plist 是 `KeyboardLayout = 0` +- **可裝 dogfood,條件:鍵盤配置=標準(大千)**。維護者這台 plist 是 `KeyboardLayout = 0` (標準),所以裝了就是安全的。**若使用者是許氏或倚天26,開啟本功能會讓中文輸入 幾乎完全失效**(實測 199/200 句全錯,字元正確率 95.8% → 0.3%)。 diff --git a/docs/REVERIFY-P3-2026-09-12.md b/docs/REVERIFY-P3-2026-09-12.md index d4acbc8..f74fe4e 100644 --- a/docs/REVERIFY-P3-2026-09-12.md +++ b/docs/REVERIFY-P3-2026-09-12.md @@ -3,7 +3,7 @@ 複驗對象:`f3-latin-completion` @ `72b5e0f`(round 3:`223817c` + `72b5e0f`) 上一輪:`docs/REVIEW-P3-2026-09-11.md`(B1–B5 blocking、N1–N17 non-blocking) 複驗方式:fresh context、只讀、自寫重現序列(真 `KeyHandler` XCTest)、6 個 mutation -機器:Mac Studio(Iris),Xcode 26.6,未安裝任何 build,未 `killall McBopomofo` +機器:Mac Studio,Xcode 26.6,未安裝任何 build,未 `killall McBopomofo` --- @@ -103,7 +103,7 @@ EnableUserPhrasesInPlainBopomofo, AllowMovingCursorWhenChoosingCandidates **這算不算污染:算,而且有實害。** 你機器上現在的值是: ``` -AddPhraseHookPath = /Users/lman/dev/mixime/build/Build/Products/Debug/McBopomofo.app/Contents/Resources/add-phrase-hook.sh +AddPhraseHookPath = ~/dev/mixime/build/Build/Products/Debug/McBopomofo.app/Contents/Resources/add-phrase-hook.sh ``` 安裝的輸入法在 `~/Library/Input Methods/McBopomofo.app`,這個路徑只可能是測試 host 寫的。 @@ -527,7 +527,7 @@ P-3 / P-4 / P-5 可以進 merge 後的 follow-up,不擋安裝。 暫時附在 `McBopomofoTests/LatinCompletionKeyHandlerTests.swift`,**已全部清除** - 執行:4 次 serial full/subset run、1 次 full parallel run、6 次 mutation run、 1 次 C++ `MixedScriptTest`(81/81 過) -- 離線重建 lexicon 排序模型交叉驗算 tier / rank(`scratchpad/lex.py`) +- 離線重建 lexicon 排序模型交叉驗算 tier / rank(暫存目錄裡的一次性 script) - `git status` 乾淨(`git checkout -- . && git clean -fd` 已執行,本報告除外) - plist:`/tmp/rv-before.xml` vs `/tmp/rv-after.xml` **完全相同** - `~/Library/Application Support/McBopomofo/`:parallel run 建出來過,**已移除**, diff --git a/docs/REVIEW-P3-2026-09-11.md b/docs/REVIEW-P3-2026-09-11.md index 54d401a..1a824d6 100644 --- a/docs/REVIEW-P3-2026-09-11.md +++ b/docs/REVIEW-P3-2026-09-11.md @@ -510,7 +510,7 @@ th + Tab + space -> buffer 'throughput ',詞庫 throughput 計次不 所以新增的 283 個 2 字母詞是死的。Rule B 只是多加一列候選、不覆寫預設中文。 - 我自己測到一個語料外的純中文 ON≠OFF:`2k7apl3` → ON `的apl`、OFF `的卯`。 這是 **Rule A**(Rule A 從不查詞典),屬於 P1 既有的 ~9% 誤判,不是詞典置換造成的回歸—— - 但這正是 Lman 第一天會踩到的東西。 + 但這正是維護者第一天會踩到的東西。 - **SCOWL 授權**:見 N16。結論是**確實允許再散布這份衍生詞表**,引用原文逐字正確。 ### 4. `complete()` 排序與效能 @@ -597,12 +597,12 @@ th + Tab + space -> buffer 'throughput ',詞庫 throughput 計次不 ``` Games SOW mode PRD SLA Phison Iris PCClaw Sunny Tool - Meet Tony per Lman EMBA Stripe PRO MOU Filco Tony + Meet Tony per EMBA Stripe PRO MOU Filco Tony ``` 其中 `Games / mode / Tool / Meet / Tony / per / Stripe / PRO / Iris / Sunny`(10 個) 小寫形都在 SCOWL 裡 → 是**排序失敗**; - `SOW / PRD / SLA / MOU / EMBA / PCClaw / Lman / Phison / Filco`(9 個)是真的縮寫/專有名詞。 + `SOW / PRD / SLA / MOU / EMBA / PCClaw / / Phison / Filco`(9 個)是真的縮寫/專有名詞。 比例約 50:50,與全量統計(62% 排序失敗)方向一致 —— **報告的三分類不準確**。 ### 9. Mutation(存活率 2/6 = 33%) @@ -621,7 +621,7 @@ th + Tab + space -> buffer 'throughput ',詞庫 throughput 計次不 ### 10. 可裝 dogfood 判定 -**不可裝。** Lman 換裝這個 build 後第一天會撞到的前 5 件事: +**不可裝。** 維護者換裝這個 build 後第一天會撞到的前 5 件事: | # | 會發生什麼 | 來源 | 判定 | |---|---|---|---| diff --git a/tools/eval/BASELINE.md b/tools/eval/BASELINE.md index 5cdbf79..0f63628 100644 --- a/tools/eval/BASELINE.md +++ b/tools/eval/BASELINE.md @@ -7,7 +7,8 @@ unmodified F1/F2 logic -- this is the *baseline*, i.e. what the stock McBopomofo engine does today, before any zh/en mixed-typing or AI re-ranking work lands. -Corpus: `/Users/lman/Dev/mixime-private/eval200.tsv` -- 200 rows (vault=200, synthetic=0). +Corpus: the maintainer's private 200-sentence corpus (not in this repo; build +your own with `tools/eval/build_corpus.py`) -- 200 rows (vault=200, synthetic=0). Language model data: `build/Build/Products/Debug/Bopomix.app/Contents/Resources/data.txt` (sha256 0deae7b7c1dcde1d7a30d139e7068543e0c0e7112e944b63eb52947bca1db7ac). ## Commands @@ -15,7 +16,7 @@ Language model data: `build/Build/Products/Debug/Bopomix.app/Contents/Resources/ ``` cmake -S Source/Engine -B build-engine -DENABLE_TEST=ON cmake --build build-engine -python3 tools/eval/run_eval.py --corpus /Users/lman/Dev/mixime-private/eval200.tsv \ +python3 tools/eval/run_eval.py --corpus /corpus.tsv \ --cli build-engine/tools/eval/bopomix-eval --data build/Build/Products/Debug/Bopomix.app/Contents/Resources ``` @@ -36,7 +37,7 @@ each gold English token still appears literally in the composed output. | latency (avg / p50 / p95 / max) | 2475us / 2517us / 4308us / 6012us | This is expected to be near 0%: the current engine has no English-awareness -at all (see zhuyin-ime-personal.md's F1 scope). Every English letter is +at all (see the design notes' F1 scope). Every English letter is also a valid standard-layout Bopomofo key, so an English word typed without switching modes is either silently dropped (composition fails `hasUnigrams`) or misrecognized as unrelated Chinese character(s) -- it is @@ -67,7 +68,7 @@ Generated: 2026-09-10T17:21:35+08:00 Same corpus and language model as the P0.5 baseline above, run with `--mixed on` (Source/Engine/MixedScript/, see -~/.claude/plans/zhuyin-ime-personal.md's P1 design section) instead of the +the design notes' P1 design section) instead of the baseline's unmodified engine. **These numbers are not acceptance criteria.** `bopomix-eval` reimplements @@ -82,7 +83,7 @@ section as an engine regression check only. ### Commands ``` -python3 tools/eval/run_eval.py --corpus /Users/lman/Dev/mixime-private/eval200.tsv \ +python3 tools/eval/run_eval.py --corpus /corpus.tsv \ --cli build-engine/tools/eval/bopomix-eval --data build/Build/Products/Debug/Bopomix.app/Contents/Resources \ --mixed on --lexicon-dir Source/Data ``` diff --git a/tools/eval/README.md b/tools/eval/README.md index d44d7a7..0395c9e 100644 --- a/tools/eval/README.md +++ b/tools/eval/README.md @@ -1,7 +1,7 @@ # bopomix eval harness (P0.5) Headless tooling to measure the engine's zh/en mixed-typing behavior -without an installed input method, per `~/.claude/plans/zhuyin-ime-personal.md`'s +without an installed input method, per the design notes' "build the harness before the feature" principle. Everything here talks to the *real* C++ engine (`Source/Engine/`) -- nothing is re-implemented in Python; Python only orchestrates the compiled CLI and does filtering/scoring. @@ -32,7 +32,7 @@ bopomix-eval --data --mode {keys|readings|keyseq} [--layout stand [--mixed on|off] [--lexicon-dir ] ``` -`--mixed on` (P1, see `zhuyin-ime-personal.md`'s P1 design section) drives +`--mixed on` (P1, see the design notes' P1 design section) drives `keys` mode through the same `Source/Engine/MixedScript/` decision engine KeyHandler.mm uses for zh/en mixed typing -- rule A (structurally impossible Bopomofo shape) and rule B (dictionary word, which only adds a @@ -59,7 +59,7 @@ once a tone marker lands (or on a space, or at end of line, simulating a trailing Enter), the pending reading is composed via `ReadingGrid:: insertReading` and, at the very end, `ReadingGrid::walk()` picks the best path. It does **not** add any English-detection logic -- that gap is -exactly what F1 (see zhuyin-ime-personal.md) is meant to close, so this +exactly what F1 (see the design notes) is meant to close, so this harness can measure it honestly. Concretely: every ASCII letter is *also* a valid standard-layout Bopomofo key, so typing an English word without switching modes gets swallowed into the reading buffer and either silently @@ -157,7 +157,24 @@ Two other divergences worth knowing about, both deliberate: acceptance measurement lives in `BopomixTests/MixedScriptKeyHandlerTests.swift` (`testEval200ThroughKeyHandler`), which types the same corpus into a - real `KeyHandler`. + real `KeyHandler`. That test and + `LatinCompletionKeyHandlerTests.testEval200LatinCompletion` read the + corpus from the `BOPOMIX_EVAL_CORPUS` environment variable and skip + themselves when it is unset or points at a file that does not exist: + + ``` + BOPOMIX_EVAL_CORPUS=/corpus.tsv \ + tools/eval/check_plist_unchanged.sh \ + xcodebuild -project Bopomix.xcodeproj -scheme Bopomix \ + -configuration Debug -derivedDataPath build \ + CODE_SIGN_IDENTITY="-" CODE_SIGNING_REQUIRED=NO DEVELOPMENT_TEAM="" test + ``` + + `xcodebuild` does not pass its own environment to the test host: only + `TEST_RUNNER_` variables get through, with the prefix stripped. + `check_plist_unchanged.sh` re-exports `BOPOMIX_EVAL_CORPUS` under that + name for you; calling `xcodebuild` directly needs + `TEST_RUNNER_BOPOMIX_EVAL_CORPUS=...` instead. ## `build_corpus.py` @@ -168,8 +185,8 @@ out-of-repo** candidate file (see Privacy below). python3 tools/eval/build_corpus.py \ --cli ./build-engine/tools/eval/bopomix-eval \ --data \ - --candidates ~/Dev/mixime-private/corpus_candidates.txt \ - --output ~/Dev/mixime-private/eval200.tsv + --candidates /corpus_candidates.txt \ + --output /corpus.tsv ``` Pipeline: @@ -210,7 +227,7 @@ list-marker glyph like "O" or "V") can still show up as its own one-letter `en` segment in a small number of rows. Not fixed in this pass; flagged for follow-up rather than silently ignored. -### Corpus format (`eval200.tsv`) +### Corpus format (`corpus.tsv`) Six tab-separated columns, no header: @@ -233,7 +250,7 @@ id sentence segments(JSON) readings keys source format artifact, not keystrokes -- see "Why a space after every syllable" above before feeding this column to anything that models the real key handling. -- `source`: `vault` (derived from Lman's own text, see Privacy) or +- `source`: `vault` (derived from your own text, see Privacy) or `synthetic` (hand-written template, safe to publish). ## `run_eval.py` @@ -242,7 +259,7 @@ Runs the two baseline metrics over a corpus TSV and writes `BASELINE.md`. ``` python3 tools/eval/run_eval.py \ - --corpus ~/Dev/mixime-private/eval200.tsv \ + --corpus /corpus.tsv \ --cli ./build-engine/tools/eval/bopomix-eval \ --data ``` @@ -273,9 +290,10 @@ whether the failure is context-dependent or a genuine rule/dictionary gap. ## Privacy -`~/Dev/mixime-private/` (candidates and the generated `eval200.tsv`) can -contain Lman's own text and must **never** enter this repo. The repo's -`.gitignore` has a blanket `*.private.*` rule as a backstop, and +The candidate file and the generated corpus TSV can contain your own text +and must **never** enter this repo. Keep them outside the working tree, or +under the ignored `eval-corpus/` directory. The repo's `.gitignore` has a +blanket `*.private.*` rule and an `eval-corpus/` rule as backstops, and `tools/eval/fixtures/sample10.tsv` (10 synthetic-only rows, regenerated by `build_corpus.py`) is the only corpus-shaped file meant to be committed here. Do not add a `--output` pointing inside this repo. diff --git a/tools/eval/bopomix_eval.cpp b/tools/eval/bopomix_eval.cpp index 3333681..4509478 100644 --- a/tools/eval/bopomix_eval.cpp +++ b/tools/eval/bopomix_eval.cpp @@ -90,7 +90,7 @@ void PrintUsage(const char* argv0) { << "\n" << "--mixed on drives `keys` mode through the same " "Source/Engine/MixedScript/ decision engine KeyHandler.mm uses " - "(see zhuyin-ime-personal.md's P1 section); it is a no-op for " + "(see the design notes' P1 section); it is a no-op for " "readings/keyseq. --mixed on requires --lexicon-dir " "containing latin-words.txt and latin-tech-seed.txt (see " "tools/lexicon/build_lexicon.py and Source/Data/).\n"; @@ -139,7 +139,7 @@ bool ParseArgs(int argc, char** argv, Args* outArgs, std::string* error) { } if (outArgs->layout != "standard") { // Only the standard (dayi... actually "Da Chien"/Standard) layout is - // supported for now. See zhuyin-ime-personal.md P0.5 scope. + // supported for now. See the design notes' P0.5 scope. *error = "only --layout standard is supported by this eval harness"; return false; } diff --git a/tools/eval/build_corpus.py b/tools/eval/build_corpus.py index e273a30..8c09af5 100644 --- a/tools/eval/build_corpus.py +++ b/tools/eval/build_corpus.py @@ -2,9 +2,9 @@ """Build tools/eval's 200-sentence chat-style zh/en mixed-typing eval corpus. Reads candidate sentences from a private, out-of-repo file (see ---candidates; defaults to ~/Dev/mixime-private/corpus_candidates.txt, which -is mostly AI-generated summaries and Slack/gmail text, NOT sentences Lman -actually typed), filters them down to short, colloquial, zh/en-mixed chat +--candidates; the maintainer's own set was mostly AI-generated summaries and +Slack/gmail text, NOT sentences the maintainer actually typed), filters them +down to short, colloquial, zh/en-mixed chat sentences, and pads the remainder up to --target with synthetic sentences generated from templates (clearly tagged source=synthetic). @@ -20,9 +20,9 @@ 4. Writes id / sentence / segments (JSON) / readings / keys / source to the output TSV. -Privacy: the output TSV can contain Lman's own text (via the vault-derived -candidates) and MUST stay under ~/Dev/mixime-private/ (see repo .gitignore's -`*.private.*` rule and tools/eval/README.md). Only tools/eval/fixtures/ +Privacy: the output TSV can contain your own text (via the vault-derived +candidates) and MUST stay out of this repo (see repo .gitignore's +`*.private.*` and `eval-corpus/` rules and tools/eval/README.md). Only tools/eval/fixtures/ sample10.tsv (synthetic-only, 10 rows) is meant to ever enter this repo. """ @@ -37,6 +37,8 @@ import sys from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parents[2] + # --------------------------------------------------------------------------- # Filtering # --------------------------------------------------------------------------- @@ -290,12 +292,14 @@ def main() -> int: parser.add_argument( "--candidates", type=Path, - default=Path("~/Dev/mixime-private/corpus_candidates.txt").expanduser(), + default=REPO_ROOT / "eval-corpus" / "corpus_candidates.txt", + help="private, out-of-VCS candidate sentences (one per line)", ) parser.add_argument( "--output", type=Path, - default=Path("~/Dev/mixime-private/eval200.tsv").expanduser(), + default=REPO_ROOT / "eval-corpus" / "corpus.tsv", + help="where to write the generated corpus TSV (never commit it)", ) parser.add_argument("--cli", type=Path, required=True, help="path to bopomix-eval") parser.add_argument("--data", type=Path, required=True, help="Resources dir with data.txt") @@ -374,7 +378,7 @@ def main() -> int: # tools/eval/fixtures/sample10.tsv is a repo-committed, synthetic-only # sample (see .gitignore's *.private.* rule and the module docstring). - # It is generated independently of whether eval200.tsv itself needed any + # It is generated independently of whether the corpus TSV itself needed any # synthetic padding, so it always has 10 rows. fixture_sentences = generate_synthetic( 10, avoid=seen_norm | {r["sentence"] for r in rows} diff --git a/tools/eval/check_plist_unchanged.sh b/tools/eval/check_plist_unchanged.sh index 6be7ecf..a72340c 100755 --- a/tools/eval/check_plist_unchanged.sh +++ b/tools/eval/check_plist_unchanged.sh @@ -38,6 +38,15 @@ set -u +# xcodebuild does not hand its own environment to the test host process: +# only variables named TEST_RUNNER_ get through, with the prefix +# stripped. So `BOPOMIX_EVAL_CORPUS=... check_plist_unchanged.sh xcodebuild +# ... test` would otherwise reach the eval tests as "unset" and skip them. +# Forward it under the name xcodebuild actually propagates. +if [ -n "${BOPOMIX_EVAL_CORPUS:-}" ]; then + export TEST_RUNNER_BOPOMIX_EVAL_CORPUS="${BOPOMIX_EVAL_CORPUS}" +fi + DOMAIN="io.github.lmanchu.inputmethod.bopomix" DATA_FOLDER="${HOME}/Library/Application Support/Bopomix" WORKDIR="$(mktemp -d -t bopomix-plist-guard)" diff --git a/tools/eval/run_eval.py b/tools/eval/run_eval.py index 54e8750..7d7d6bd 100644 --- a/tools/eval/run_eval.py +++ b/tools/eval/run_eval.py @@ -23,7 +23,7 @@ problem that F1 measures. Usage: - python3 tools/eval/run_eval.py --corpus \\ + python3 tools/eval/run_eval.py --corpus \\ --cli ./build-engine/tools/eval/bopomix-eval --data """ @@ -302,7 +302,7 @@ def stats_line(label: str, values: list[int]) -> str: import datetime if mixed: - # P1 (see zhuyin-ime-personal.md): append to the existing + # P1 (see the design notes): append to the existing # BASELINE.md rather than overwriting it, so the P0.5 baseline # numbers above stay intact for comparison. Run with --mixed off # first (the default) to (re)generate that baseline section. @@ -324,7 +324,7 @@ def stats_line(label: str, values: list[int]) -> str: Same corpus and language model as the P0.5 baseline above, run with `--mixed on` (Source/Engine/MixedScript/, see -~/.claude/plans/zhuyin-ime-personal.md's P1 design section) instead of the +the design notes' P1 design section) instead of the baseline's unmodified engine. **These numbers are not acceptance criteria.** `bopomix-eval` reimplements @@ -424,7 +424,7 @@ def stats_line(label: str, values: list[int]) -> str: | latency (avg / p50 / p95 / max) | {statistics.mean(f1_latencies_us):.0f}us / {percentile(f1_latencies_us, 0.5):.0f}us / {percentile(f1_latencies_us, 0.95):.0f}us / {max(f1_latencies_us)}us | This is expected to be near 0%: the current engine has no English-awareness -at all (see zhuyin-ime-personal.md's F1 scope). Every English letter is +at all (see the design notes' F1 scope). Every English letter is also a valid standard-layout Bopomofo key, so an English word typed without switching modes is either silently dropped (composition fails `hasUnigrams`) or misrecognized as unrelated Chinese character(s) -- it is diff --git a/tools/lexicon/build_lexicon.py b/tools/lexicon/build_lexicon.py index 5ad05ce..15357d8 100644 --- a/tools/lexicon/build_lexicon.py +++ b/tools/lexicon/build_lexicon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """Builds Source/Data/latin-words.txt for the zh/en mixed-typing feature -(see ~/.claude/plans/zhuyin-ime-personal.md's "P1 設計"/"P3 設計" sections +(see the design notes' "P1 設計"/"P3 設計" sections and Source/Engine/MixedScript/latin_lexicon.h). Source (2026-09-11 P3 fix-round rework): SCOWL/ESDB itself (the "Spell From 467edb45b3a2209961259ac5f190d249b141144c Mon Sep 17 00:00:00 2001 From: lmanchu Date: Sun, 13 Sep 2026 20:22:07 +0800 Subject: [PATCH 2/2] chore(privacy): replace real names in corpus dumps and drop private codenames Follow-up to the previous scrub. The review reports quote samples of the maintainer's private eval corpus verbatim, which leaked colleague names and the customers/partners those notes are about; the shipped tech word list carried the maintainer's own product codenames. - Corpus token dumps and frequency counts in docs/REVIEW-P3-2026-09-11.md and docs/REVERIFY-P3-2026-09-12.md now use stable placeholders: people become , the maintainer's company/customers/partners become , their products become . The same word always maps to the same placeholder, so the counts and the "how many of these are real proper nouns" conclusions still read correctly. Generic global brands already in the shipped lexicon (google, computex, Stripe, Apple, Electron) stay as they are. - Source/Data/latin-tech-seed.txt: dropped irisgo, tachikoma, hermes, gloria, apollo, argus, wells and magi. The first two are private names that exist nowhere else; the other six are internal system codenames that were outranking the whole dictionary for words a public user has no reason to want boosted. All six remain available from latin-words.txt. Ranks are explicit per row, so the resulting gaps change nothing. - tools/eval/build_corpus.py: the synthetic-padding token list no longer names the maintainer's company, and says to swap in your own. Verified: xcodebuild build succeeds; 255 tests green under check_plist_unchanged.sh without BOPOMIX_EVAL_CORPUS (2 skipped), with PLIST_UNCHANGED and DATA_FOLDER_UNCHANGED; the built app's bundled latin-tech-seed.txt no longer contains any of the removed terms. Co-Authored-By: Claude Opus 5 (1M context) --- Source/Data/latin-tech-seed.txt | 8 -------- docs/REVERIFY-P3-2026-09-12.md | 12 ++++++------ docs/REVIEW-P3-2026-09-11.md | 8 ++++---- tools/eval/build_corpus.py | 5 ++++- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Source/Data/latin-tech-seed.txt b/Source/Data/latin-tech-seed.txt index 891ce39..dd151e3 100644 --- a/Source/Data/latin-tech-seed.txt +++ b/Source/Data/latin-tech-seed.txt @@ -24,15 +24,7 @@ github 12 notion 13 zoom 14 xcode 15 -irisgo 16 openrouter 17 -tachikoma 18 -hermes 19 -gloria 20 -apollo 21 -argus 22 -wells 23 -magi 24 computex 25 oem 26 dau 27 diff --git a/docs/REVERIFY-P3-2026-09-12.md b/docs/REVERIFY-P3-2026-09-12.md index f74fe4e..dc7aadc 100644 --- a/docs/REVERIFY-P3-2026-09-12.md +++ b/docs/REVERIFY-P3-2026-09-12.md @@ -58,7 +58,7 @@ P1 時代是 append,所以換資料夾只會合併不會刪。 - plist 沒被改(`PLIST_UNCHANGED`),**但** - `~/Library/Application Support/McBopomofo/`(跑之前不存在)被建立, 裡面 `latin-user.txt` 1.1K,內容是 eval200 corpus 的 130 個詞、score 2: - `irisgo 22 / google 28 / computex 12 / acer 10 / asus 10 / tony 8 / claude 8 / phison 6 …` + ` 22 / google 28 / computex 12 / 10 / 10 / 8 / 8 / 6 …` - 檔案 mtime `10:05:13`,parallel run 的 xcresult 是 `Test-McBopomofo-2026.09.12_10-05-08` ——parallel 跑起來 5 秒就寫下去了 @@ -367,9 +367,9 @@ never completable 155/330 **抽樣 30 個 ranking miss,全部 `isLatinWord(lowercased)` = true**(都真的在詞典、小寫後在): ``` -Games SOW mode SLA Iris Sunny Tool Meet Tony per Stripe PRO Tony love you -Community comments cli Rita Lucy More Electron Wanda redline code RAG Tony -Abby App Family +Games SOW mode SLA Tool Meet per Stripe PRO love you +Community comments cli More Electron redline code RAG + App Family ``` **但其中 15 個從頭到尾沒有 lock 成 Latin run**(見 P-4): @@ -453,8 +453,8 @@ KeyHandlerBopomofoTests.swift:844/868/872: (AssociatedPhrases state) 決定性的一條:`latin-user.txt` 的斷言失敗訊息裡,出現的是**別的 worker 的**詞: ``` -2 letters is below the floor: ["apple":4, "nitrosense":4, "tony":10, "irixion":2, - "biilabs":2, "lecun":2, "audrey":2, "cofounder":2, ...] +2 letters is below the floor: ["apple":4, "":4, "":10, "":2, + "":2, "":2, "":2, "cofounder":2, ...] ``` 我的測試讀的是自己 UUID 的 throwaway 資料夾,裡面卻出現 eval corpus 的詞—— diff --git a/docs/REVIEW-P3-2026-09-11.md b/docs/REVIEW-P3-2026-09-11.md index 1a824d6..a3a6ed7 100644 --- a/docs/REVIEW-P3-2026-09-11.md +++ b/docs/REVIEW-P3-2026-09-11.md @@ -596,13 +596,13 @@ th + Tab + space -> buffer 'throughput ',詞庫 throughput 計次不 - **154 個不可補全 token 抽樣人工分類**(抽 20): ``` - Games SOW mode PRD SLA Phison Iris PCClaw Sunny Tool - Meet Tony per EMBA Stripe PRO MOU Filco Tony + Games SOW mode PRD SLA Tool + Meet per EMBA Stripe PRO MOU ``` - 其中 `Games / mode / Tool / Meet / Tony / per / Stripe / PRO / Iris / Sunny`(10 個) + 其中 `Games / mode / Tool / Meet / / per / Stripe / PRO / / `(10 個) 小寫形都在 SCOWL 裡 → 是**排序失敗**; - `SOW / PRD / SLA / MOU / EMBA / PCClaw / / Phison / Filco`(9 個)是真的縮寫/專有名詞。 + `SOW / PRD / SLA / MOU / EMBA / / / / `(9 個)是真的縮寫/專有名詞。 比例約 50:50,與全量統計(62% 排序失敗)方向一致 —— **報告的三分類不準確**。 ### 9. Mutation(存活率 2/6 = 33%) diff --git a/tools/eval/build_corpus.py b/tools/eval/build_corpus.py index 8c09af5..45a2daa 100644 --- a/tools/eval/build_corpus.py +++ b/tools/eval/build_corpus.py @@ -129,8 +129,11 @@ def segment(norm: str) -> list[dict]: # Synthetic sentence generation (used to pad up to --target) # --------------------------------------------------------------------------- +# Swap the product/company names here for the proper nouns that actually +# recur in your own text -- the synthetic padding is only useful if it +# looks like the English you really type. EN_TOKENS = [ - "Mac", "Slack", "PR", "API", "IrisGo", "OpenRouter", "GitHub", "Notion", + "Mac", "Slack", "PR", "API", "Figma", "OpenRouter", "GitHub", "Notion", "Zoom", "Xcode", "demo", "bug", "repo", "commit", "deploy", "cache", "token", "prompt", "agent", "model", "build", "merge", "review", "sprint", "CLI", "CI", "UI", "UX", "PM", "OKR",