Skip to content

Latest commit

 

History

History
120 lines (100 loc) · 37.5 KB

File metadata and controls

120 lines (100 loc) · 37.5 KB

AGENTS.md

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.

1. Project Snapshot

  • 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 surface InputSession_DarwinSurface, session-controller bindings, SessionHost wiring, 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 the vChewing_OSNeutral_LibVanguard package since the 2026-09-13 merge.
    • vChewing_Tekkon(模組 Tekkon): Keyboard parsers, Zhuyin/Bopomofo composer, stroke cassette parser, phonabet utilities — likewise a target of vChewing_OSNeutral_LibVanguard.
    • vChewing_LexiconAssembly(模組 LexiconAssembly): LM instantiation facade, user phrase memory, perception override, associated phrases — likewise a target of vChewing_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_BrailleSputnik packages as same-named targets, shipping as one dynamic library. Module names are unchanged, so import 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 and vChewing-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 — named VanguardSwiftExtension after its packaging role — while the module keeps its original name, so import SwiftExtension stays 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 (from vChewing-VanguardLexicon repository). Compiled factory lexicons (.txtMap + .revlookup pairs) are injected into vChewing_MainAssembly4Darwin during build-time. The runtime backend is VanguardTrie.TextMapTrie (sorted-array key index with binary search, on-demand VALUES parsing, bounded parsed-entry cache).

2. Environment & Build Paths

  • Authoritative toolchain: Swift 6.4+ (Package.swift declares swift-tools-version 6.4; CI runs on macos-26 with 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 support libArcLite nor approachable concurrency, and they demand explicit @MainActor on conformances in a default-isolated package (#ConformanceIsolation) — see vChewing-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.sdk that Command Line Tools carries (/Library/Developer/CommandLineTools/SDKs/MacOSX13.3.sdk — LEGACY_SDK names 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), targeting x86_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 (--sdk never reaches build-plugin compilation, so the 5.10 compiler would otherwise be handed a macOS 27 SDK and die on could 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 .a archives — an OpenSource 5.10 toolchain can only precompile those — and since an archive carries no load command, platforms: nil costs nothing there. Each subpackage carries Package@swift-5.10.swift plus the Package@swift-6.0 / 6.1 / 6.2 / 6.3 blocker manifests and its own lowercase makefile (build510 / clean510) so one package can be checked in isolation; the aggregate vChewing_OSNeutral_LibVanguard's set — six manifests plus its makefile — is the reference implementation of that layout (its makefile is currently byte-identical to vChewing-LibVanguard's root makefile; that is an observation, not a standing invariant). vChewing_MainAssembly4Darwin's 5.10 manifest omits the remote vChewing-VanguardLexicon dependency and its two build plugins, while Package.swift keeps them — both halves measured on 2026-09-16: that lexicon's host tools use concurrency (AsyncThrowingStream, withTaskGroup, Actor, …), so the package declares platforms: [.macOS(.v10_15)]; lowering that to the 10.13 floor SwiftPM 5.10 falls back to produces 125 availability errors (82 of them concurrency 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 imports LibVanguardChewingData / 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: its CSQLite3 target carried unsafeFlags, 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 — vChewing and vChewingInstallerLegacy (renamed so it never collides with the 6.4-side vChewingInstaller) — because those two are the only artifacts linked on this path. make debugLegacy builds one x86_64 slice at macOS 10.9 — fast to iterate on, but unoptimised, hence sluggish in use; make releaseLegacy builds that slice and an arm64-apple-macosx11.0 one and lipos 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 same Build/Products/Legacy/, and leaving the assembly to a hand-run make bundleLegacy — whose default config is debug — 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 claim sdk 10.9 while 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 why vChewingInstallerLegacy had no dark mode while the IME was saved by its NSRequiresAquaSystemAppearance key. 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-loads LegacyZone/ARCLite/libarclite_macosx.a; that flag therefore lives in the Makefile (which knows the triple), not in the manifest. It has to be -Xlinker -force_load in any case: the Swift driver, unlike clang, never injects libArcLite, and -L + -larclite_macosx alone 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 — though libswift_Concurrency is no longer among them: since 2026-09-16 the whole closure holds zero concurrency references (0 of 20 archives per slice, 0 undefined swift_task_* in either executable), so no Task-shaped stray reaches the load commands. Since macOS 10.9 ships no Swift runtime, the BundleAppsLegacy plugin (verb bundle-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/Frameworks to each executable's LC_RPATH. It then assembles the two .app bundles under Build/Products/Legacy/ — vChewing.app and vChewingInstallerLegacy.app, the latter embedding the former in its Contents/Resources/ — each with its own copy of the runtime in Contents/Frameworks/ and @executable_path/../Frameworks appended to its rpath (make bundleLegacy). Both Info.plists also get the DT* 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 honours DEVELOPER_DIR) — and resolves its $(…) variables live from the SDK named by --sdk (which make bundleLegacy passes as LEGACY_SDK), so the bundles report DTSDKName = macosx13.3, DTXcode = 1540. Omit --sdk and they simply carry no such record. LSMinimumSystemVersion is deliberately not copied from that template — it stays pinned to legacyDeploymentTarget. It also drops any absolute LC_RPATH the 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 is Legacy/ and not Build/Products/<Config>/ because the IME bundle has to keep the name vChewing.app, which BundleApps owns one level up. The IME takes its lexicon assets from LegacyZone/LexiconBuildTrigger/Build/ — its own manifest is lexicon-free, so those three files come from the separate lexiconLegacy target (see the env notes below) — and writes them inside MainAssembly4Darwin_MainAssembly4Darwin.bundle, which is what Bundle.currentSPM resolves and what the modern build's injector plugins fill too (nothing goes flat into Contents/Resources/; the modern vChewing.app does not carry them there either). SwiftPM 5.10 writes a resource bundle flat, so that directory is the bundle's resourceURL, while 6.x wraps the payload in Contents/Resources/, and the plugin follows whichever shape the build produced. The flat shape also comes with no Info.plist at all, so the plugin stamps one at the bundle root: ResourceLocator resolves every resource through Bundle(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/swift first, @executable_path/../Frameworks second): on a modern macOS the system runtime wins — /usr/lib/swift/*.dylib no 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 shared SwiftExtension one; the legacy repo mirrors that arrangement file-for-file (its LibVanguard/Session/SessionProtocol.swift and InputSession_HandleDisplay.swift are byte-identical to this repo's), even though a single Xcode target could never hit the cross-module -O SIL 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 declare 10.9 (legacyDeploymentTarget in Plugins/BundleAppsLegacy/plugin.swift), the x86_64 compile-time target is x86_64-apple-macosx10.9 (LEGACY_X86_TRIPLE in the root Makefile; LEGACY_TRIPLE in all 20 per-package makefiles), and every legacy artifact must report 10.9 for LSMinimumSystemVersion, minos and sdk. Holding that floor costs exactly three #available(macOS 10.10, *) guards, all in vChewing_SettingsUI (NSViewController.addChild ×9, the three NSSearchField properties, NSColor.secondaryLabelColor); the arm64 slice stays at 11.0, which is as low as arm64 goes. When a new AppKit API sits above the floor, copy vChewing-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.
  • 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*.swift with the toolchain it ships and offers no way to hand resolution to the system's default one. The manifest declares swift-tools-version: 6.4 and 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 in README*.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 to dispatch_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 cost vChewingInstallerLegacy a 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_SDK names 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 the ld warnings that print the SDK path on the CLT side). Note that this SDK is a legacy one that current CLTs no longer ship — their CLTools_SDK_macOS13 component 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: bundleLegacy depends on the lexiconLegacy target, which runs LegacyZone/LexiconBuildTrigger with LEXICON_CONFIG=release. That variable is deliberately not named LEGACY_CONFIG — the root LEGACY_CONFIG is 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-version 6.4) via Package.swift root manifest. App bundle assembly and universal binary scripting via Makefile with BundleApps CommandPlugin (the 5.10 path uses BundleAppsLegacy instead).
  • CLI builds:
    • Universal binary release: make release (builds arm64 + x86_64, creates signed .app bundles in Build/Products/Release/).
    • Archive with dSYMs: make archive (creates .xcarchive in Xcode Archives folder).
    • Debug native build: make debug (single-arch; .app bundles output under Build/Products/Debug/).
    • Package-only tests: cd Packages/vChewing_OSNeutral_LibVanguard && swift build && swift test.
  • First-time setup: make update (fetches/generates lexicons) then make release.
  • Xcode builds share Build/Products/: vChewing.xcodeproj (schemes vChewing, vChewingDebuggable, vChewingInstaller) resolves its products into the repo's own Build/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). Setting SYMROOT in 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, BundleApps removes only the two app bundles it owns and leaves everything else (Xcode's vChewingDebuggable.app, per-target .o/.swiftmodule blobs, .app.dSYM) untouched.
  • Dynamic products: the typing closure ships as one dynamic library from Packages/vChewing_OSNeutral_LibVanguard (.library(name: "Vanguard", type: .dynamic, …), product file libVanguard.dylib; package named LibVanguard, aggregate target/module LibVanguard — that is the one the app-side packages import). The utility module SwiftExtension (shipped as the dynamic product VanguardSwiftExtension) is deliberately not part of that closure: it lives in its own nested package Packages/vChewing_OSNeutral_LibVanguard/Deps/VanguardSwiftExtension/ and ships as a second dynamic library (libVanguardSwiftExtension.dylib), which vChewing.app links alongside the first while vChewingInstaller.app links 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 because VanguardSwiftExtension has types on OSFrameworkImpl's public surface (@ArrayBuilder in a public init, plus a retroactive DynamicProperty conformance on AppProperty) and because PrefMgr carries ~103 @AppProperty properties inside the closure. On non-Darwin platforms that package's product is .static, so libVanguard.dylib embeds it there and no second library is produced. Both vChewing.app and vChewingInstaller.app are assembled through the shared embedLinkedDylibs(of:into:availableDylibs:), which embeds only the dylibs the executable actually links (otool -L → @rpath/*.dylib) into Contents/Frameworks/, adds @executable_path/../Frameworks via install_name_tool before 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 injects com.apple.security.cs.disable-library-validation only 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.framework under Build/Products/<Config>/PackageFrameworks/, links it as @rpath/Vanguard.framework/Versions/A/Vanguard, and embeds it into Contents/Frameworks/ on its own — nothing in the project requests this and no BundleApps phase is involved. Because the vChewing and vChewingInstaller targets build with ENABLE_HARDENED_RUNTIME = YES while this machine signs ad-hoc, that product would die at launch under library validation (dyld: … different Team IDs, exit 134), so both targets set RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES in Debug and Release — Xcode's build-setting form of the entitlement above. Xcode injects it at signing time and it never reaches Sources/vChewingIME_macOS/Resources/vChewing.entitlements or Sources/Installer_macOS/Resources/vChewingInstaller.entitlements, which is what BuildPKG.sh re-signs from, so distributed builds stay free of the relaxation. vChewingDebuggable needs no exception: it builds with ENABLE_HARDENED_RUNTIME = NO.

3. Repository Layout (quick map)

  • Packages/vChewing_MainAssembly4Darwin/.../SessionController/: IMK-facing Darwin surface. InputSession_DarwinSurface.swift holds the IMK entry surface (init(controller:), recognizedEvents, showPreferences, handleNSEvent(NSEvent) conversion, IMKInputController surface, toggleInputMode TIS logic); SessionControllerSputnik.swift binds IMKInputSessionController to the session and forwards callbacks; SessionHostWiring.swift wires SessionHost closures (called from MainSputnik4IME.init).
  • Packages/vChewing_OSNeutral_LibVanguard/Sources/LibVanguard/InputHandler/: FSM split across triage, composition, candidate handling, and commissions; InputHandler.swift is the concrete handler class.
  • Packages/vChewing_OSNeutral_LibVanguard/Sources/LibVanguard/Session/: OS-independent session system — SessionCoreProtocol (shared session base protocol with switchState()/resetInputHandler() defaults), SessionProtocol + InputSession (the session class), IMEState factories / IMEStateParsed, SessionHost (host-injection point for all OS-dependent actions), SessionClientProxy (cross-platform client-proxy abstraction). Darwin-specific behavior lives in MainAssembly4Darwin via SessionHost wiring + 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-purpose SwiftExtension utilities, kept as a nested subpackage inside the aggregate's own directory (SwiftPM accepts nested packages); placing it at Deps/… rather than as a sibling under Packages/ makes .package(path:) resolve to the same relative path here and in vChewing-LibVanguard, which is what keeps the two manifests byte-identical. Package name and product name are VanguardSwiftExtension; the module/target name stays SwiftExtension, so call sites keep import SwiftExtension.
  • Packages/vChewing_OSFrameworkImpl/: AppKit result-builder DSL for SettingsCocoa window, etc.
  • Packages/vChewing_SettingsUI/: Preferences UI as a standalone package — SwiftUI SettingsUI for macOS 14+ (incl. the phrase editor and the About pane) plus the AppKit SettingsCocoa alternate; host actions are injected via SettingsUIHost closures (SettingsUIHostWiring.swift in MainAssembly).
  • Packages/vChewing_CandidateWindow/: The Candidate window.
  • Plugins/BundleApps/: CommandPlugin that assembles .app bundles and optional .xcarchive archives (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/Frameworks to the two legacy executables' LC_RPATH (their @rpath/libswift* records stay untouched, so the system runtime still wins on modern macOS), and assembles Build/Products/Legacy/vChewing.app + vChewingInstallerLegacy.app with the same runtime under Contents/Frameworks/ and @executable_path/../Frameworks on their rpaths; invoked as swift package … bundle-apps-legacy -- --build-dir …, or as make bundleLegacy (see the comment above that target, and §2 for the rationale). Its --archive flag additionally writes Build/Products/vChewingInstallerLegacy-<year>-<month>-<day>-<HHMM>hrs.xcarchive — the installer (which embeds the IME) under Products/Applications/, a dSYM per executable under dSYMs/, and an Info.plist carrying ArchiveVersion/ApplicationProperties — in the same shape BundleApps writes 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), so make archiveLegacy moves 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 build510 builds it with the 5.10 toolchain (no --triple, no -target: one arch, host-only) and make collect copies what it yields — VanguardFactoryDict4Typing.txtMap, template-associatedPhrases-chs.txt, template-associatedPhrases-cht.txt — into Build/ for the legacy .app assembly. It exists because the app's own 5.10 manifest must stay lexicon-free (§2) — and since 2026-09-16 the repo-root Makefile runs it for you: the lexiconLegacy target is a prerequisite of bundleLegacy, 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 own CFBundleName per language (vChewing Installer Legacy / 唯音紀念版安裝程式 / 唯音纪念版安装程式 / 唯音入力 紀念版 実装用アプリ), copied from vChewing-OSX-legacy. BundleAppsLegacy merges them over the .lproj directories it copies out of Sources/Installer_macOS/Resources/, so the two installers stop looking alike in Finder while the bundle on disk stays vChewingInstallerLegacy.app. They live here rather than next to their siblings on purpose: Sources/Installer_macOS is an Xcode PBXFileSystemSynchronizedRootGroup, 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's NSHumanReadableCopyright — the string the About pane prints as its copyright line. The legacy vChewing.app shares Sources/vChewingIME_macOS/Resources/ with the modern one, so the marker cannot live in the source; BundleAppsLegacy merges Aqua Special Build. © 2021 and onwards The vChewing Project. over the .lproj directories it copies out of there, for Base / en / ja / zh-Hans / zh-Hant (Base is included because the IME, unlike the installer, ships one). The value is verbatim from vChewing-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, in InstallerShared.swift's mainWindowTitle). Both override directories feed one helper, mergeLegacyLocalizedOverrides(into:overridesFrom:), which overwrites only the keys a given file names and leaves CFEULAContent untouched.
  • Makefile: Root-level automation for universal binary builds (swift build --arch arm64/x86_64, lipo merge), lexicon toolchain integration, and CommandPlugin invocation.
  • Sources/Installer_macOS/ + Packages/vChewing_InstallerAssembly4Darwin/: SwiftUI installer app (the vChewingInstaller executable) + pkg resources; BuildPKG.sh assembles the installer PKG.

4. Runtime Flow & Key Concepts

  1. Event capture: IMK instantiates IMKInputSessionController (vChewing_IMKUtils); SessionControllerSputnik forwards its NSEvents to the bound InputSession, which converts NSEvent→KBEvent (InputSession_DarwinSurface.handleNSEvent) and marshals them into KBEvent structures for the portable session core.
  2. FSM triage: InputHandler in LibVanguard interprets events, orchestrates Tekkon composer, updates the Homa assembler, and switches IMEState instances.
  3. Composer: Tekkon manages Zhuyin/phonetic/stroke buffers, auto-correction, cassette mode, and exposes inline display strings.
  4. Assembler: Homa Assembler builds DAG segments, snapshots perception intelligences, exposes candidate / consolidation / revolver APIs, and emits assembledSentence for UI rendering.
  5. Language Models: LXAssembly merges factory lexicons (Vanguard TextMap format, served by VanguardTrie.TextMapTrie in the package-local TrieKit target), user phrases, exclusion lists, associated phrase suggestions, POM (perception / fading-memory) n-gram statistics, and perception override suggestions.
  6. UI update: InputSession refreshes candidate window, composition buffer, tooltips, notifications, symbol menu.

Reference algorithm.md for the deep algorithm write-up (zh-Hant).

5. Development Guardrails

  • 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 -CHS file'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, and PrefMgr together. Avoid naked UserDefaults.standard access except in constrained scenarios.
  • User data paths: Avoid hard-coded user data paths except where necessary in package test targets.
  • State machine: Prefer new IMEState enum cases and explicit transition APIs over boolean shortcuts. SessionCoreProtocol (LibVanguard) provides switchState()/resetInputHandler() default implementations shared by mock tests and production; extend InputHandlerProtocol for per-event triage logic.
  • Conditional APIs: Guard platform-specific code (#if canImport(Darwin)) as needed; keep Linux compatibility in LibVanguard package and its local dependencies.
  • Bundle resources: SPM #bundle / Bundle.module is avoided in the LibVanguard closure — SwiftPM's generated accessor calls fatalError when the resource bundle is missing, which breaks dynamic-library loading and relocated builds. Use ResourceLocator (target of the same name in vChewing_OSNeutral_LibVanguard) instead: it probes the host bundle, the anchor bundle, the sibling directory, and the loaded image's directory, and returns nil on failure. Bundle.currentSPM (MainAssembly4Darwin) is a nullable wrapper over it. Hosts may point the lookup at explicit locations via ResourceProvision (LibVanguard), ResourceLocator.specifyResourceBundleURL(_:forBundleNamed:), or BPMFVS.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*.swift manifest or a makefile is 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. The LibVanguard dependency closure keeps its own LGPL v3.0 license files with the Swift static-linking exception, while VanguardSwiftExtension ships 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 from vChewing-VanguardLexicon repository) and injected into vChewing_MainAssembly4Darwin at build-time via SPM build plugins. The runtime backend is VanguardTrie.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.

6. Testing Expectations

  • 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-package makefiles expose no test goals. Tests run on the 6.4+ side only; the 5.10 side is exercised with make build510 (subpackages) and make debugLegacy (the two repo-root executables).
  • LibVanguard and MainAssembly packages host end-to-end style tests; consider snapshotting PrefMgr state before/after.
  • When touching Tekkon or Homa, craft stress tests covering multi-syllable input, perception overrides, cursor edge cases.
  • Use swift test --filter to run targeted suites when debugging CI regressions.

7. Contribution Workflow

  • Commit format: ModuleName // SubModuleName: Change. (Conventional Commit semantics kept terse.) Example: LibVanguard // FSM: Fix cursor guard.
  • Devlogs prose is zh-Hant-TW: vChewing-DevLogs records 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 --amend or 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.sh must remain sandbox safe.

8. Quick Reference Checklist

  • Honor language restrictions in new text.
  • Update .strings when adding user-visible strings.
  • Gate new APIs through protocols as needed.
  • Run relevant swift test targets.
  • 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.