This handbook briefs AI coding assistants on the vChewing (唯音) macOS repository. Use only English or zh-Hant-TW for docs/comments/reviews; zh-Hans is allowed only in filename stems ending with -CHS, and there the required style is zh-Hans-TW — simplify the characters only and keep Taiwan vocabulary.
- Purpose: Native Zhuyin / Bopomofo input method for macOS with optional phonetic and stroke keyboards, simplified ↔ traditional isolation, and sandboxed distribution installers.
- Implementation: Pure Swift modules layered on AppKit/IMK. C(++)/ObjC(++) bridges exist only where Swift cannot interface directly with legacy assets.
- Primary packages:
vChewing_MainAssembly4Darwin: IMK front-end (Darwin surfaceInputSession_DarwinSurface, session-controller bindings,SessionHostwiring, UI bridges, sandbox glue).vChewing_OSNeutral_LibVanguard: Typing FSM, session core protocol (SessionCoreProtocol), Tekkon integration, user preference wiring, cassette/stroke handling.vChewing_Homa(模組Homa): DAG-DP assembler (sentence assembler) with candidate override, consolidation, revolver, and perception hooks — a target of thevChewing_OSNeutral_LibVanguardpackage since the 2026-09-13 merge.vChewing_Tekkon(模組Tekkon): Keyboard parsers, Zhuyin/Bopomofo composer, stroke cassette parser, phonabet utilities — likewise a target ofvChewing_OSNeutral_LibVanguard.vChewing_LexiconAssembly(模組LexiconAssembly): LM instantiation facade, user phrase memory, perception override, associated phrases — likewise a target ofvChewing_OSNeutral_LibVanguard.- That package also holds the whole typing closure: since 2026-09-13 it carries the former
vChewing_Shared、vChewing_LexiconAssembly、vChewing_Homa、vChewing_Tekkon、vChewing_BPMFVS、vChewing_BrailleSputnikpackages as same-named targets, shipping as one dynamic library. Module names are unchanged, soimport Shared/import Tekkon/ … still work; only the package that vends them changed. Packages/vChewing_OSNeutral_LibVanguard/Deps/VanguardSwiftExtension/(模組SwiftExtension): general-purpose Swift/Foundation utilities. It sits inside the aggregate directory as a nested subpackage (SwiftPM accepts nested packages), which keeps the.package(path:)string identical in this repo andvChewing-LibVanguard— that is what lets the two manifests stay byte-identical. It was folded into the aggregate in the same 2026-09-13 merge, but was extracted again because the App-based installer needed only this module and was otherwise forced to carry the whole typing engine; see the Dynamic products note below. It ships as its own dynamic product — namedVanguardSwiftExtensionafter its packaging role — while the module keeps its original name, soimport SwiftExtensionstays the spelling everywhere.- Packages that stay separate include
vChewing_OSFrameworkImpl(AppKit result-builder DSL),vChewing_SettingsUI,vChewing_CandidateWindow, and the rest of the Darwin-side surface.
- Lexicon assets: Provided by remote Swift Package plugin
VanguardTextMapPlugin(fromvChewing-VanguardLexiconrepository). Compiled factory lexicons (.txtMap+.revlookuppairs) are injected intovChewing_MainAssembly4Darwinduring build-time. The runtime backend isVanguardTrie.TextMapTrie(sorted-array key index with binary search, on-demand VALUES parsing, bounded parsed-entry cache).
- Authoritative toolchain: Swift 6.4+ (
Package.swiftdeclaresswift-tools-version6.4; CI runs onmacos-26with Xcode 27.0 pinned). Swift 6.2 and 6.3 are deliberately blocked by per-package blocker manifests (Package@swift-6.2.swift/Package@swift-6.3.swift): they neither supportlibArcLitenor approachable concurrency, and they demand explicit@MainActoron conformances in a default-isolated package (#ConformanceIsolation) — seevChewing-DevLogs/Research/Phase217_SOP.md. - Runtime target: macOS 12 Monterey and newer. Older macOS support lives in another repo.
- Legacy Swift 5.10 path: an additional, local-only path that exists so a macOS 10.9 build can be produced at all: the Swift 5.10 toolchain with the
MacOSX13.3.sdkthat Command Line Tools carries (/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk—LEGACY_SDKnames it; the copy inside Xcode 15 is only a symlink to it, and that SDK is a legacy one: current CLTs ship a remover for it instead, so a clean machine has to obtain it from the macOS 13.3 CLT or Xcode 14.3), targetingx86_64-apple-macosx10.9(the floor this path exists to serve — see the note at the end of this bullet), run with an old-enough Xcode as the active developer directory (--sdknever reaches build-plugin compilation, so the 5.10 compiler would otherwise be handed a macOS 27 SDK and die oncould not build Objective-C module 'Foundation'; the binding is to Xcode 15.4, but any Xcode whose default SDK is ≤ 14.x — i.e. ≤ 15.4 — is equivalent, and the requirement cannot be replaced by CLT or a hand-made developer directory, both measured on 2026-09-16). It has two layers:- Subpackages stop at static
.aarchives — an OpenSource 5.10 toolchain can only precompile those — and since an archive carries no load command,platforms: nilcosts nothing there. Each subpackage carriesPackage@swift-5.10.swiftplus thePackage@swift-6.0/6.1/6.2/6.3blocker manifests and its own lowercasemakefile(build510/clean510) so one package can be checked in isolation; the aggregatevChewing_OSNeutral_LibVanguard's set — six manifests plus itsmakefile— is the reference implementation of that layout (itsmakefileis currently byte-identical tovChewing-LibVanguard's rootmakefile; that is an observation, not a standing invariant).vChewing_MainAssembly4Darwin's 5.10 manifest omits the remotevChewing-VanguardLexicondependency and its two build plugins, whilePackage.swiftkeeps them — both halves measured on 2026-09-16: that lexicon's host tools use concurrency (AsyncThrowingStream,withTaskGroup,Actor, …), so the package declaresplatforms: [.macOS(.v10_15)]; lowering that to the 10.13 floor SwiftPM 5.10 falls back to produces 125 availability errors (82 of themconcurrency is only available in macOS 10.15), and leaving the declaration in place makes the 5.10 resolver reject our 10.13-floor package (depends on the product 'VanguardTextMapPlugin' which requires macos 10.15). Raising our floor instead would not help either: SwiftPM derives each target's deployment target from its own package's declaration, so the lexicon's errors would remain. The omission costs nothing — nothing here importsLibVanguardChewingData/VCDataBuilder/VanguardTrieKit, the two plugins only inject factory-lexicon resources into the modern app's bundle, and the legacy host brings its own lexicons; until the lexicon splits its plugins from its data library, this asymmetry stands. (Before 4.8.0 that dependency was rejected outright for a third reason: itsCSQLite3target carriedunsafeFlags, which SwiftPM 5.10 refuses in any plugin product of a non-root package. SOP §13.4 / §14.) - The repo root (
Package@swift-5.10.swift) is the one 5.10 manifest that does produce executables —vChewingandvChewingInstallerLegacy(renamed so it never collides with the 6.4-sidevChewingInstaller) — because those two are the only artifacts linked on this path.make debugLegacybuilds one x86_64 slice at macOS 10.9 — fast to iterate on, but unoptimised, hence sluggish in use;make releaseLegacybuilds that slice and anarm64-apple-macosx11.0one andlipos each executable into a universal binary. Both targets finish by assembling their own pair of bundles ($(MAKE) bundleLegacy LEGACY_CONFIG=<cfg>): the two configurations write into the sameBuild/Products/Legacy/, and leaving the assembly to a hand-runmake bundleLegacy— whose default config isdebug— silently shipped unoptimised binaries once. Both also pass-Xlinker -platform_version -Xlinker macos -Xlinker <min> -Xlinker <sdkVersion>(the version read from$(LEGACY_SDK)/SDKSettings.plist): SwiftPM 5.10 fills that slot from the triple, so without the override the executables claimsdk 10.9while being linked against the 13.3 SDK — and AppKit reads that field to decide whether an app is old enough to be locked into Aqua, which is precisely whyvChewingInstallerLegacyhad no dark mode while the IME was saved by itsNSRequiresAquaSystemAppearancekey. Arm64 has no macOS below 11.0 and its ARC runtime has lived in libobjc since macOS 11, so only the x86_64 leg force-loadsLegacyZone/ARCLite/libarclite_macosx.a; that flag therefore lives in theMakefile(which knows the triple), not in the manifest. It has to be-Xlinker -force_loadin any case: the Swift driver, unlike clang, never injects libArcLite, and-L+-larclite_macosxalone is inert (libobjc already exports the ARC entry points). Entry points:make debugLegacy/make releaseLegacy/make archiveLegacy/make cleanLegacy, scratch.build/.legacy-root. Their Swift-runtime records (@rpath/libswift*.dylib) are left untouched — thoughlibswift_Concurrencyis no longer among them: since 2026-09-16 the whole closure holds zero concurrency references (0 of 20 archives per slice, 0 undefinedswift_task_*in either executable), so noTask-shaped stray reaches the load commands. Since macOS 10.9 ships no Swift runtime, theBundleAppsLegacyplugin (verbbundle-apps-legacy, declared only in the 5.10 manifest) collects the back-deployment dylibs from every Xcode under/Applications— the minOS 10.9 copies under.../usr/lib/swift-5.0|5.5/macosx/— copies them into<build-dir>/Frameworks/and appends@loader_path/Frameworksto each executable'sLC_RPATH. It then assembles the two.appbundles underBuild/Products/Legacy/—vChewing.appandvChewingInstallerLegacy.app, the latter embedding the former in itsContents/Resources/— each with its own copy of the runtime inContents/Frameworks/and@executable_path/../Frameworksappended to its rpath (make bundleLegacy). Both Info.plists also get theDT*build-environment keys Xcode normally writes (DTXcode,DTSDKBuild,DTSDKName,DTPlatformVersion,BuildMachineOSBuild, …): the plugin reads the platform's own template at<sdk>/../../Info.plist→AdditionalInfo— the very table Xcode stamps from; a Command Line Tools SDK has no platform around it, so there the plugin falls back to the active developer directory's platform (xcode-select -p, which honoursDEVELOPER_DIR) — and resolves its$(…)variables live from the SDK named by--sdk(whichmake bundleLegacypasses asLEGACY_SDK), so the bundles reportDTSDKName = macosx13.3,DTXcode = 1540. Omit--sdkand they simply carry no such record.LSMinimumSystemVersionis deliberately not copied from that template — it stays pinned tolegacyDeploymentTarget. It also drops any absoluteLC_RPATHthe build machine left behind — SwiftPM records the toolchain that supplied the runtime as…/<toolchain>/usr/lib/swift-5.5/macosx— keeping/usr/lib/swift,@loader_path*and@executable_path/../Frameworks. That directory isLegacy/and notBuild/Products/<Config>/because the IME bundle has to keep the namevChewing.app, whichBundleAppsowns one level up. The IME takes its lexicon assets fromLegacyZone/LexiconBuildTrigger/Build/— its own manifest is lexicon-free, so those three files come from the separatelexiconLegacytarget (see the env notes below) — and writes them insideMainAssembly4Darwin_MainAssembly4Darwin.bundle, which is whatBundle.currentSPMresolves and what the modern build's injector plugins fill too (nothing goes flat intoContents/Resources/; the modernvChewing.appdoes not carry them there either). SwiftPM 5.10 writes a resource bundle flat, so that directory is the bundle'sresourceURL, while 6.x wraps the payload inContents/Resources/, and the plugin follows whichever shape the build produced. The flat shape also comes with noInfo.plistat all, so the plugin stamps one at the bundle root:ResourceLocatorresolves every resource throughBundle(url:), and a bare directory is not a shape Foundation promises to accept as a bundle. That reproduces the two-tier lookup the legacy Xcode build installs (/usr/lib/swiftfirst,@executable_path/../Frameworkssecond): on a modern macOS the system runtime wins —/usr/lib/swift/*.dylibno longer exists as files and is served from the dyld shared cache — while 10.9–10.13 fall through to the bundled copies.CGRect.seniorTheBeast— the non-zero-size rect that works around clients mishandling an empty composition rect — is a file-private constant at the bottom of each file that needs it, never a sharedSwiftExtensionone; the legacy repo mirrors that arrangement file-for-file (itsLibVanguard/Session/SessionProtocol.swiftandInputSession_HandleDisplay.swiftare byte-identical to this repo's), even though a single Xcode target could never hit the cross-module-OSIL mismatch that forced the split here. The deployment floor is macOS 10.9 and it is not negotiable — supporting it is the only reason this path exists. Both legacy bundles declare10.9(legacyDeploymentTargetinPlugins/BundleAppsLegacy/plugin.swift), the x86_64 compile-time target isx86_64-apple-macosx10.9(LEGACY_X86_TRIPLEin the rootMakefile;LEGACY_TRIPLEin all 20 per-package makefiles), and every legacy artifact must report10.9forLSMinimumSystemVersion,minosandsdk. Holding that floor costs exactly three#available(macOS 10.10, *)guards, all invChewing_SettingsUI(NSViewController.addChild×9, the threeNSSearchFieldproperties,NSColor.secondaryLabelColor); the arm64 slice stays at11.0, which is as low as arm64 goes. When a new AppKit API sits above the floor, copyvChewing-OSX-Legacy's shape — that repo is the 10.9-compliant reference — and never raise the floor instead. That repo has been frozen since vChewing 4.8.0 (the legacy distro is built here now), so read it as a reference only and never mirror changes back into it.
- Subpackages stop at static
- It leaves the two bullets above untouched: Swift 6.4+ stays the authoritative toolchain and macOS 12+ stays the runtime target. Swift 5.10 builds stay out of CI: each runner would have to install the extra toolchain and the Xcode 15 SDK at no small cost, and the 5.10 side serves compilability and legacy-runtime assembly locally. Environment pairing and the rest of the rollout:
vChewing-DevLogs/Research/Phase217_SOP.md. - Xcode cannot resolve packages with an OpenSource toolchain: it always parses
Package*.swiftwith the toolchain it ships and offers no way to hand resolution to the system's default one. The manifest declaresswift-tools-version: 6.4and Intel Macs top out at Xcode 26.x (< 6.4), so on Intel Macs Xcode is no longer a usable build entry point for this repo — build with the SwiftPM CLI (make spmDebug/make release) against a 6.4+ toolchain instead. Apple silicon is unaffected (Xcode 27 ships 6.4). This corrects the earlier "Xcode may lag a release behind as long as a newer toolchain is installed" claim inREADME*.md: true for compiling, false for resolving. - Three things bite only on the macOS 10.9 path: ① Never hand libdispatch an Optional block here. The Swift 5.0 back-deployment shim these bundles embed routes
…setEventHandler(handler: nil)straight todispatch_source_set_event_handler(source, NULL), and 10.9's libdispatch BUGs on a NULL block (NULL was passed where a block should have been, SIGILL); pass{}instead. That costvChewingInstallerLegacya crash seconds after pressing Install until 2026-09-16 — the modern runtime tolerates NULL, so nothing on the 6.4 side ever showed it. ②LEGACY_SDKnames the Command Line Tools copy, not an Xcode one:/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk(Xcode 15 bundles macOS 14 itself and only symlinks to that path — hence theldwarnings that print the SDK path on the CLT side). Note that this SDK is a legacy one that current CLTs no longer ship — theirCLTools_SDK_macOS13component is a remover (CLTools_macOS_DevSDK_Remove_macOS13.pkg) — so a fresh machine has to place it there by hand (from the macOS 13.3 CLT or Xcode 14.3); the toolchain × SDK pairing still has to be a 13.x SDK. ③ Lexicon assets build themselves:bundleLegacydepends on thelexiconLegacytarget, which runsLegacyZone/LexiconBuildTriggerwithLEXICON_CONFIG=release. That variable is deliberately not namedLEGACY_CONFIG— the rootLEGACY_CONFIGis the app configuration and leaks into sub-makes as a target-specific variable, which drags the lexicon build back to debug. - Build system: Swift Package Manager (SwiftPM,
swift-tools-version6.4) viaPackage.swiftroot manifest. App bundle assembly and universal binary scripting viaMakefilewithBundleAppsCommandPlugin (the 5.10 path usesBundleAppsLegacyinstead). - CLI builds:
- Universal binary release:
make release(builds arm64 + x86_64, creates signed .app bundles inBuild/Products/Release/). - Archive with dSYMs:
make archive(creates.xcarchivein Xcode Archives folder). - Debug native build:
make debug(single-arch;.appbundles output underBuild/Products/Debug/). - Package-only tests:
cd Packages/vChewing_OSNeutral_LibVanguard && swift build && swift test.
- Universal binary release:
- First-time setup:
make update(fetches/generates lexicons) thenmake release. - Xcode builds share
Build/Products/:vChewing.xcodeproj(schemesvChewing,vChewingDebuggable,vChewingInstaller) resolves its products into the repo's ownBuild/Products/<Config>/— identical to the plugin's output path. The location is NOT declared in the repo; it comes from a machine-level Xcode preference (IDECustomBuildProductsPath=Build/Products,IDECustomBuildIntermediatesPath=Build/Intermediates.noindex,IDECustomBuildLocationType=RelativeToWorkspace). SettingSYMROOTin the project instead is not an option: Xcode then falls back to a legacy build location and SPM integration stops working entirely (Packages are not supported when using legacy build locations). Because the two producers share one directory,BundleAppsremoves only the two app bundles it owns and leaves everything else (Xcode'svChewingDebuggable.app, per-target.o/.swiftmoduleblobs,.app.dSYM) untouched. - Dynamic products: the typing closure ships as one dynamic library from
Packages/vChewing_OSNeutral_LibVanguard(.library(name: "Vanguard", type: .dynamic, …), product filelibVanguard.dylib; package namedLibVanguard, aggregate target/moduleLibVanguard— that is the one the app-side packagesimport). The utility moduleSwiftExtension(shipped as the dynamic productVanguardSwiftExtension) is deliberately not part of that closure: it lives in its own nested packagePackages/vChewing_OSNeutral_LibVanguard/Deps/VanguardSwiftExtension/and ships as a second dynamic library (libVanguardSwiftExtension.dylib), whichvChewing.applinks alongside the first whilevChewingInstaller.applinks only the second. Two reasons: (a) the App-based installer needs nothing else from the closure, yet folding the module in forced it to carry the whole typing engine; (b) SwiftPM only links a product dynamically across package boundaries — a target belonging to a same-package dynamic product is still statically merged into same-package consumers, which would silently duplicate the module in one process. That duplicate is unacceptable here becauseVanguardSwiftExtensionhas types onOSFrameworkImpl's public surface (@ArrayBuilderin apublic init, plus a retroactiveDynamicPropertyconformance onAppProperty) and becausePrefMgrcarries ~103@AppPropertyproperties inside the closure. On non-Darwin platforms that package's product is.static, solibVanguard.dylibembeds it there and no second library is produced. BothvChewing.appandvChewingInstaller.appare assembled through the sharedembedLinkedDylibs(of:into:availableDylibs:), which embeds only the dylibs the executable actually links (otool -L→@rpath/*.dylib) intoContents/Frameworks/, adds@executable_path/../Frameworksviainstall_name_toolbefore signing, and signs inner-out (dylib first, then the app) without--deep. Because ad-hoc signing leaves the process with no Team ID, the plugin injectscom.apple.security.cs.disable-library-validationonly when frameworks are embedded; the distribution path re-signs every nested binary with a real Team ID (BuildPKG.sh) and needs no such relaxation. Note that SwiftPM rejects any arrangement where a module is linked statically by both the app and the dynamic product (duplication of library code) — that is why the closure lives in a single package with a single dynamic product. - Xcode builds embed the same library differently: Xcode resolves the SPM dynamic product as
Vanguard.frameworkunderBuild/Products/<Config>/PackageFrameworks/, links it as@rpath/Vanguard.framework/Versions/A/Vanguard, and embeds it intoContents/Frameworks/on its own — nothing in the project requests this and noBundleAppsphase is involved. Because thevChewingandvChewingInstallertargets build withENABLE_HARDENED_RUNTIME = YESwhile this machine signs ad-hoc, that product would die at launch under library validation (dyld: … different Team IDs, exit 134), so both targets setRUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YESin Debug and Release — Xcode's build-setting form of the entitlement above. Xcode injects it at signing time and it never reachesSources/vChewingIME_macOS/Resources/vChewing.entitlementsorSources/Installer_macOS/Resources/vChewingInstaller.entitlements, which is whatBuildPKG.shre-signs from, so distributed builds stay free of the relaxation.vChewingDebuggableneeds no exception: it builds withENABLE_HARDENED_RUNTIME = NO.
Packages/vChewing_MainAssembly4Darwin/.../SessionController/: IMK-facing Darwin surface.InputSession_DarwinSurface.swiftholds the IMK entry surface (init(controller:),recognizedEvents,showPreferences,handleNSEvent(NSEvent)conversion, IMKInputController surface,toggleInputModeTIS logic);SessionControllerSputnik.swiftbindsIMKInputSessionControllerto the session and forwards callbacks;SessionHostWiring.swiftwiresSessionHostclosures (called fromMainSputnik4IME.init).Packages/vChewing_OSNeutral_LibVanguard/Sources/LibVanguard/InputHandler/: FSM split across triage, composition, candidate handling, and commissions;InputHandler.swiftis the concrete handler class.Packages/vChewing_OSNeutral_LibVanguard/Sources/LibVanguard/Session/: OS-independent session system —SessionCoreProtocol(shared session base protocol withswitchState()/resetInputHandler()defaults),SessionProtocol+InputSession(the session class),IMEStatefactories /IMEStateParsed,SessionHost(host-injection point for all OS-dependent actions),SessionClientProxy(cross-platform client-proxy abstraction). Darwin-specific behavior lives in MainAssembly4Darwin viaSessionHostwiring + the Darwin surface.Packages/vChewing_OSNeutral_LibVanguard/Sources/Homa/: Assembler core (Homa_Assembler.swift,Homa_PathFinder.swift, candidate/consolidation APIs, etc.).Packages/vChewing_OSNeutral_LibVanguard/Sources/Tekkon/: Keyboard parsers, composer, Zhuyin constants.Packages/vChewing_OSNeutral_LibVanguard/Sources/LexiconAssembly/: LM instantiators, perception override, associated phrase derivation.Packages/vChewing_OSNeutral_LibVanguard/Deps/VanguardSwiftExtension/: the general-purposeSwiftExtensionutilities, kept as a nested subpackage inside the aggregate's own directory (SwiftPM accepts nested packages); placing it atDeps/…rather than as a sibling underPackages/makes.package(path:)resolve to the same relative path here and invChewing-LibVanguard, which is what keeps the two manifests byte-identical. Package name and product name areVanguardSwiftExtension; the module/target name staysSwiftExtension, so call sites keepimport SwiftExtension.Packages/vChewing_OSFrameworkImpl/: AppKit result-builder DSL for SettingsCocoa window, etc.Packages/vChewing_SettingsUI/: Preferences UI as a standalone package — SwiftUISettingsUIfor macOS 14+ (incl. the phrase editor and the About pane) plus the AppKitSettingsCocoaalternate; host actions are injected viaSettingsUIHostclosures (SettingsUIHostWiring.swiftin MainAssembly).Packages/vChewing_CandidateWindow/: The Candidate window.Plugins/BundleApps/: CommandPlugin that assembles.appbundles and optional.xcarchivearchives (codesigning, entitlements, SPM bundle filtering).Plugins/BundleAppsLegacy/: its Swift 5.10 counterpart — collects the back-deployment Swift runtime dylibs from the two back-deployment runtime directories of each Xcode under/Applications, copies them into<build-dir>/Frameworks/, appends@loader_path/Frameworksto the two legacy executables'LC_RPATH(their@rpath/libswift*records stay untouched, so the system runtime still wins on modern macOS), and assemblesBuild/Products/Legacy/vChewing.app+vChewingInstallerLegacy.appwith the same runtime underContents/Frameworks/and@executable_path/../Frameworkson their rpaths; invoked asswift package … bundle-apps-legacy -- --build-dir …, or asmake bundleLegacy(see the comment above that target, and §2 for the rationale). Its--archiveflag additionally writesBuild/Products/vChewingInstallerLegacy-<year>-<month>-<day>-<HHMM>hrs.xcarchive— the installer (which embeds the IME) underProducts/Applications/, a dSYM per executable underdSYMs/, and anInfo.plistcarryingArchiveVersion/ApplicationProperties— in the same shapeBundleAppswrites for the modern distro; the plugin cannot place it in~/Library/Developer/Xcode/Archives/itself (a command plugin may only write inside the package directory), somake archiveLegacymoves it there.LegacyZone/ARCLite/libarclite_macosx.a: LibARCLite as extracted from Xcode 14.2 (Xcode 14.3 dropped it from the toolchain). The 5.10 manifest force-loads it into both legacy executables;LegacyZone/Build/is gitignored scratch.LegacyZone/LexiconBuildTrigger/: a throwaway host-only SwiftPM package (swift-tools-version: 5.10,platforms: [.macOS(.v10_15)]— that declaration is the crux, since the lexicon's plugin products require 10.15) whose single target hosts the remote lexicon's two build-tool plugins purely to trigger their build.make build510builds it with the 5.10 toolchain (no--triple, no-target: one arch, host-only) andmake collectcopies what it yields —VanguardFactoryDict4Typing.txtMap,template-associatedPhrases-chs.txt,template-associatedPhrases-cht.txt— intoBuild/for the legacy.appassembly. It exists because the app's own 5.10 manifest must stay lexicon-free (§2) — and since 2026-09-16 the repo-rootMakefileruns it for you: thelexiconLegacytarget is a prerequisite ofbundleLegacy, so both legacy build targets satisfy it automatically. It builds--configuration release(LEXICON_CONFIG, see §2), because the lexicon's data-build step is CPU-heavy and a debug plugin is markedly slower. 4.8.0 no longer produces a.revlookup; the plugin re-runs on every invocation (~35 s in debug; ~49 s when the whole release build is cold), having no inputs to compare against.LegacyZone/InstallerLocalizations/<lproj>/InfoPlist.strings: the legacy distro installer's ownCFBundleNameper language (vChewing Installer Legacy/唯音紀念版安裝程式/唯音纪念版安装程式/唯音入力 紀念版 実装用アプリ), copied fromvChewing-OSX-legacy.BundleAppsLegacymerges them over the.lprojdirectories it copies out ofSources/Installer_macOS/Resources/, so the two installers stop looking alike in Finder while the bundle on disk staysvChewingInstallerLegacy.app. They live here rather than next to their siblings on purpose:Sources/Installer_macOSis an XcodePBXFileSystemSynchronizedRootGroup, so a new.lproj-bearing directory under it would be swept into the modern installer's copy-resources phase as well.LegacyZone/IMELocalizations/<lproj>/InfoPlist.strings: the mirror image of the bullet above, for the IME'sNSHumanReadableCopyright— the string the About pane prints as its copyright line. The legacyvChewing.appsharesSources/vChewingIME_macOS/Resources/with the modern one, so the marker cannot live in the source;BundleAppsLegacymergesAqua Special Build. © 2021 and onwards The vChewing Project.over the.lprojdirectories it copies out of there, forBase/en/ja/zh-Hans/zh-Hant(Baseis included because the IME, unlike the installer, ships one). The value is verbatim fromvChewing-OSX-legacy, whose About pane shows the very same string — its installer lproj deliberately carry no such prefix, that distro marking itself in the installer's window title instead (Aqua Special, inInstallerShared.swift'smainWindowTitle). Both override directories feed one helper,mergeLegacyLocalizedOverrides(into:overridesFrom:), which overwrites only the keys a given file names and leavesCFEULAContentuntouched.Makefile: Root-level automation for universal binary builds (swift build --arch arm64/x86_64,lipomerge), lexicon toolchain integration, and CommandPlugin invocation.Sources/Installer_macOS/+Packages/vChewing_InstallerAssembly4Darwin/: SwiftUI installer app (thevChewingInstallerexecutable) + pkg resources;BuildPKG.shassembles the installer PKG.
- Event capture: IMK instantiates
IMKInputSessionController(vChewing_IMKUtils);SessionControllerSputnikforwards its NSEvents to the boundInputSession, which converts NSEvent→KBEvent (InputSession_DarwinSurface.handleNSEvent) and marshals them intoKBEventstructures for the portable session core. - FSM triage:
InputHandlerin LibVanguard interprets events, orchestrates Tekkon composer, updates the Homa assembler, and switchesIMEStateinstances. - Composer: Tekkon manages Zhuyin/phonetic/stroke buffers, auto-correction, cassette mode, and exposes inline display strings.
- Assembler: Homa Assembler builds DAG segments, snapshots perception intelligences, exposes candidate / consolidation / revolver APIs, and emits
assembledSentencefor UI rendering. - Language Models:
LXAssemblymerges factory lexicons (Vanguard TextMap format, served byVanguardTrie.TextMapTriein the package-localTrieKittarget), user phrases, exclusion lists, associated phrase suggestions, POM (perception / fading-memory) n-gram statistics, and perception override suggestions. - UI update:
InputSessionrefreshes candidate window, composition buffer, tooltips, notifications, symbol menu.
Reference algorithm.md for the deep algorithm write-up (zh-Hant).
- Language: Code comments, docs, and commit messages in English or zh-Hant. (zh-Hans only in files if filenamestem ends with
-CHS— and there the style is zh-Hans-TW: character-level simplification only, Taiwan vocabulary preserved.支援stays支援, never支持;記憶體→记忆体, never内存;硬碟→硬碟, never硬盘. Rewording a-CHSfile's vocabulary into Mainland usage is drift, not a fix.) - UI: AppKit by default — no Interface Builder nibs/storyboards, and AppKit windows are implemented with the AppKit Result Builder DSL (
vChewing_OSFrameworkImpl). Exceptions: the SwiftUI settings surface (vChewing_SettingsUI, macOS 14+) and the SwiftUI installer app (vChewing_InstallerAssembly4Darwin). Keep UI work on the main actor. - Preferences: Extend
UserDef,PrefMgrProtocol, andPrefMgrtogether. Avoid nakedUserDefaults.standardaccess except in constrained scenarios. - User data paths: Avoid hard-coded user data paths except where necessary in package test targets.
- State machine: Prefer new
IMEStateenum cases and explicit transition APIs over boolean shortcuts.SessionCoreProtocol(LibVanguard) providesswitchState()/resetInputHandler()default implementations shared by mock tests and production; extendInputHandlerProtocolfor per-event triage logic. - Conditional APIs: Guard platform-specific code (
#if canImport(Darwin)) as needed; keep Linux compatibility inLibVanguardpackage and its local dependencies. - Bundle resources: SPM
#bundle/Bundle.moduleis avoided in theLibVanguardclosure — SwiftPM's generated accessor callsfatalErrorwhen the resource bundle is missing, which breaks dynamic-library loading and relocated builds. UseResourceLocator(target of the same name invChewing_OSNeutral_LibVanguard) instead: it probes the host bundle, the anchor bundle, the sibling directory, and the loaded image's directory, and returnsnilon failure.Bundle.currentSPM(MainAssembly4Darwin) is a nullable wrapper over it. Hosts may point the lookup at explicit locations viaResourceProvision(LibVanguard),ResourceLocator.specifyResourceBundleURL(_:forBundleNamed:), orBPMFVS.specifyDataURL(_:). - ObjC(++)/C(+=) style: Follow Google Style Guide formatting for Objective-C(++) and C(++).
- Licensing: New Swift source files carry the 3-line MulanPSL-2.0 banner (Swift sources only: a
Package*.swiftmanifest or amakefileis not a Swift file and takes none), which cites only the SPDX identifier (// (c) <year> and onwards The vChewing Project (MulanPSL-2.0 License)./// ====================/// This code is released under the SPDX-License-Identifier: `MulanPSL-2.0`.); the license text itself lives in./LICENSE.txt. TheLibVanguarddependency closure keeps its own LGPL v3.0 license files with the Swift static-linking exception, whileVanguardSwiftExtensionships under MulanPSL-2.0; respect each module's own license and avoid mixing incompatible license assets. - Lexicon tooling: Factory lexicons are compiled by remote
VanguardTextMapPlugin(Swift Package plugin fromvChewing-VanguardLexiconrepository) and injected intovChewing_MainAssembly4Darwinat build-time via SPM build plugins. The runtime backend isVanguardTrie.TextMapTrie(sorted-array key index with binary search, on-demand VALUES parsing, bounded parsed-entry cache). Do not modify or commit generated lexicon assets; they are transient build artifacts.
- Unit tests live alongside each Swift package (
swift test). Focus on deterministic cases that mirror reported issues. - Unit tests are not open to the Swift 5.10 build path: its manifests declare no test targets (not even test-only material targets such as
LXAssemblyMaterials4Tests/HomaSharedTestComponents) and per-packagemakefiles expose no test goals. Tests run on the 6.4+ side only; the 5.10 side is exercised withmake build510(subpackages) andmake debugLegacy(the two repo-root executables). - LibVanguard and MainAssembly packages host end-to-end style tests; consider snapshotting
PrefMgrstate before/after. - When touching Tekkon or Homa, craft stress tests covering multi-syllable input, perception overrides, cursor edge cases.
- Use
swift test --filterto run targeted suites when debugging CI regressions.
- Commit format:
ModuleName // SubModuleName: Change.(Conventional Commit semantics kept terse.) Example:LibVanguard // FSM: Fix cursor guard. - Devlogs prose is zh-Hant-TW:
vChewing-DevLogsrecords are written in Traditional Chinese (Taiwan) --KnowledgeMemo4LLM.md,DevReqsHistory.md,Reqs4LLM/,Research/. Do not narrate in Japanese, English, or Simplified Chinese. Exceptions: the owner's words quoted verbatim, i18n strings and terminology tables, and code/paths/API names. Replies to the owner are zh-Hant-TW too; the language they write in is not a cue to switch. - Never push: commits are yours to make, pushes are the repository owner's. Several
repositories move together here and more than one remote is configured; report the commit
hashes and stop. Do not treat a push as having happened when judging whether
--amendor a history rewrite is safe. - Reviews: Highlight functional impact, state machine ramifications, and test coverage. Mention regression risk if tests are missing.
- Dependencies: Prefer SwiftPM-targeted adjustments. When external patches are unavoidable, document rationale in code comments and PR description.
- Installer: Keep pkg scripts idempotent.
pkgPreInstall.sh/pkgPostInstall.shmust remain sandbox safe.
- Honor language restrictions in new text.
- Update
.stringswhen adding user-visible strings. - Gate new APIs through protocols as needed.
- Run relevant
swift testtargets. - Align new keyboard layouts with Tekkon parsers and symbol tables.
Questions from contributors should reference this file first; escalate only when guidance is missing or conflicting.
For the P217 Swift 5.10 rollout, vChewing-DevLogs/Research/Phase217_SOP.md carries the implementation details (environment pairing, per-package manifests and makefiles, syntax migration, SwiftUI gating); where it is silent, this file governs, and a conflict between the two must be escalated rather than resolved silently.