Tinycast 工程规范完全指南:Posture、不可妥协项与完成标准解读

Tinycast 工程规范完全指南:Posture、不可妥协项与完成标准解读 Tinycast 工程规范完全指南Posture、不可妥协项与完成标准解读【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast本篇技术指南以 Tinycast 仓库根目录的 AGENTS.md 为核心系统拆解这个完全原生、零第三方依赖的 macOS 启动器launcher项目在工程组织上的全部硬性规则为什么它只面向最新版 macOS、AppCore为何是唯一状态所有者、Model/层为何被编译级强制保持纯净以及一个改动达到什么标准才算真正完成。读完你既能按这套规范参与 Tinycast 的代码维护也能把其中的单一所有权 分层纯净 可机械验证的完成标准方法论迁移到自己的 Swift 项目中。项目定位一个刻意保持小的 macOS 菜单栏启动器Tinycast 是一个原生 macOS 菜单栏启动器模糊匹配的应用启动器、全局与按应用热键、文本/图片剪贴板历史、内联计算器、浮动笔记、snippets、quicklinks、窗口管理与 emoji 选择器并能在 JavaScriptCore 中原生运行 Raycast 扩展。技术栈是 SwiftUI AppKit以无 Dock 图标的 accessory 形式运行LSUIElement零第三方依赖见 README.md 与 AGENTS.md 开头。但从工程角度看这个项目最值得研究的不是功能列表而是它如何用规范把代码库规模压到极小。仓库的 AGENTS.md 正是这套规范的源头它定义了全局 Posture、目录地图、十条不可妥协项、命名与注释约定以及提交前的完成检查清单。以下是逐节解读。Posture只支持最新版永远latest-only, alwaysAGENTS.md 的 Posture: latest-only, always 是全项目最根本的工程决策Tinycast targets one macOS — the current stable release — and nothing else.macOS 26Xcode 26 工具链Swift 6 语言模式。没有兼容性下限要守护、没有 shim 层、没有废弃技术债。这一决策直接体现在工程配置中project.yml 里deploymentTarget.macOS: 26.0、SWIFT_VERSION: 6.0、SWIFT_STRICT_CONCURRENCY: complete。它带来的具体写码规则是优先使用现代 Apple API用 Observation 而不是ObservableObject用 Swift Concurrency 而不是DispatchQueue或 completion handler用SMAppService而不是登录项 shim用结构化并发而不是脱离管理的零散任务。迁移绝不包装当一个 API 出现现代替代品时直接采用新 API 并删除旧调用点。保留旧写法的包装层是这个项目花最大力气清除的东西。废弃 API 即缺陷A deprecated API is a defect, not a warning to live with不是可以容忍的警告。不加兼容层、不留遗留变通、不采用旧架构模式删除优于弃用提高最低 macOS 版本就等于删掉了支持旧版本的代码。除非被明确要求永不主动引入向后兼容没有版本开关、没有迁移脚手架、没有以防万一的回退。代码库本身不携带任何迁移代码。Carbon 的两个能力缺口特例唯一例外是 Carbon但这是刻意选择的能力缺口依赖而非惯性现代 API 中没有任何东西能注册系统级组合键chord而 HIToolbox 的 TIS API 仍是枚举与选择键盘输入法的公开机制。详细论证见 docs/standards.md#posture 以及 docs/architecture.md 中的描述——每个原始 C 指针在进入 actor 代码前都会被解码为纯值。为什么如此严格docs/standards.md#posture 给出了理由兼容性下限不是一次性成本。每个 shim 都会比需要它的平台活得更久、被下一个看到的 feature 复制、把一行调用变成没人敢删的一层。Tinycast 没有外部 API、没有插件面、只支持一个 OS所以它没有需要兼容的东西——这正是它保持小的全部原因。文档还记录了一个可验证的经验事实项目已删除的版本门控代码一贯比它所门控的功能本身还要大。目录导览Where things areAGENTS.md 用两张表给出了全仓库地图这是理解规范落在哪个文件的入口文件夹存放内容Tinycast/App/main、AppDelegate、AppCore—— 组合根composition rootTinycast/DesignSystem/共享视觉原语Theme.swift是唯一设计令牌design token来源Tinycast/Platform/系统 shimPermissions、AppPaths、Signposts、NotificationToken等Tinycast/Palette/palette 外壳panel、window controller、RootPaletteView、PaletteScreenTinycast/Windows/非 palette 的 AppKit 表面Dialog/、HUD/、About/、AppWindowControllerTinycast/Features/每个 feature 一个文件夹较大的拆分为Model/Service/UI/Settings/Tests/独立测试 harness——每个 Swift 文件一个没有 XCTest targetScripts/所有可执行脚本测试运行器、数据生成器、打包、lint、编辑器配置配套的读什么文档索引表同样关键——它定义了改动前的强制阅读路径在你做这件事之前读它改动任何 wiring 或所有权结构docs/architecture.md写 Swift —— 命名、风格、并发、预算、注释docs/standards.md声称一个改动已完成docs/testing.md构建、运行或重新生成数据docs/development.md新增或重排任何视图docs/ui.md触碰某个 feature 的内部docs/features/每个文件开头都有其 invariants打包或发布构建docs/release.md值得注意的是 docs/architecture.md 提供的完整目录树App/是组合根DesignSystem/与Platform/是两个不得依赖任何 feature的共享层Features/下每个 feature 自成一体Resources/里提交了嵌入的RaycastRuntime.generated.js所以构建应用永远不需要 Node。不可妥协项Non-negotiables十条不能破坏的底线AGENTS.md 的核心是十条没有明确任务就绝不能破坏的规则feature 专属的 invariants 在各自 feature 的文档里以## Invariants章节出现。下面逐条展开并给出仓库中的实现证据。1.AppCore是唯一所有者新的长生命周期状态必须放在AppCore上在start()中完成 wiring——绝不创建与之竞争的 singleton。视图通过Environment访问 feature 的coordinator而不是AppCore。源码印证Tinycast/App/AppCore.swift 是一个MainActor Observable的final class AppCorestatic let shared持有约二十个 storeAppIndex、ClipboardStore、SnippetsStore、QuicklinkStore等、管理器/监控器/时钟ClipboardManager、HotKeyManager、HyperKeyTap、RunningAppsMonitor、SnippetKeywordListener、共享状态AppSettings、PaletteState等以及二十个 feature coordinator。它的start()方法AppCore.swift#L220-L363就是整个应用的启动序列一屏可读。配合 AppDelegate.swift#L11-L13applicationDidFinishLaunching只做一件事AppCore.shared.start()。这是唯一的 wiring 点。coordinator 全部声明为ObservationIgnored private(set) lazy——这正是 docs/architecture.md 强调的memo 缓存和惰性构建的协作对象必须加ObservationIgnored否则读取 memo 会注册依赖、导致视图在自身缓存填充时重渲染。2.Model/纯净层编译强制而非约定Features/*/Model/下的文件不得 import AppKit 或 SwiftUI且所有环境事实时钟、文件系统、主目录、汇率都以注入参数传入。这条规则的独特之处在于它由编译强制Tests/的 harness 直接编译仓库里正版的源码而非副本所以一旦Model/泄漏了 AppKit/SwiftUI importharness 立刻编译失败。相关命令见 docs/testing.md#purity-checksgrep -rln import AppKit\|import SwiftUI\|import Cocoa Tinycast/Features/*/Model/这条 grep 必须返回空。典型例证CalcEngine通过now/calendar/rates参数注入时钟与汇率见 docs/architecture.md 对Model/层的定义只依赖 Foundation外加数据需要的 SQLite3 或 CoreGraphics环境事实全部注入——这是做决策的层Service/是执行动作的层所有AXUIElement、CGEventTap、NSWorkspace.open、URLSession都住在这里。3. Swift 6 语言模式数据竞争即硬错误MainActor是默认跨 actor 的模型类型是Sendable重活或 IO 活作为nonisolated函数由Task.detached驱动到主线程外。不要引入第二个 actor。工程配置SWIFT_STRICT_CONCURRENCY: completeSWIFT_VERSION: 6.0project.yml落实了这一点。更细的并发约定在 docs/standards.md#concurrency-and-lifetime任何长生命周期Task都要存储在stop()或deinit中取消无主的Task是带额外步骤的泄漏block observer 走 RAII 的NotificationTokenPlatform/NotificationToken.swift每个捕获self的逃逸闭包用[weak self]或闭包不可能比 owner 活得久时用[unowned self]如AppCore的 coordinator wiring。ClipboardStore用isolated deinit做 SQLite 拆除——这是资源必须在其 actor 上拆除的样板写法。4. 深色是基线Theme.Colors通过ramp/adaptive按 appearance 解析每个深色值都是强制深色构建当初发布的Color.white.opacity(…)是重申而非重新推导。自由调整浅色分支只有任务本身是改深色时才动深色分支。AppAppearance驱动NSApp.appearance.system映射为nil让 AppKit 自行跟随 macOS。源码印证见 AppCore.swift#L586-L588applyAppearance()即NSApp.appearance settings.appearance.nsAppearance注释明确.system解析为nilAppKit 无轮询地跟随 macOS。设计令牌的唯一来源是 Tinycast/DesignSystem/Theme.swift。5. 自己呈现对话框绝不使用系统控件问题走DialogController报告走HUDPresenterHUD。从不使用NSAlert、NSSlider或系统 popover。docs/architecture.md 进一步解释这是承重的对话框是 borderless 的DialogPanel由DialogController驱动是全应用唯一的确认/失败/取值 presenter呈现是async的所以不阻塞主 actor且 presenter 在已有对话框时拒绝第二个——靠这一点、而不是靠一个 flag阻止了按住热键堆叠对话框。AppCore上的showNotice、confirm、reportFailure、showMessage、pickVolume都是转发器AppCore.swift#L644-L711确保DialogController与MessageHUDController单一所有。6. 网络会话与备份安全联网 feature 使用私有的.ephemeral、urlCache nil会话绝不使用URLSession.shared让自己的缓存文件成为磁盘上唯一副本。CurrencyRateStore是参照实现——复制它不要发明第二种形态。授予能力的 flag 绝不随备份携带snippetsEnabled被排除在设置备份之外防止导入备份时意外授予按键监听能力。这条在 docs/testing.md 的手动回归清单中有对应验证项snippetsEnabledis not in the exported file, and importing does not enable snippets。7. 扩展隔离不透明的盒子扩展相关的一切视图、行、菜单、几何与尺寸常量都写在Features/Extensions/内部绝不放进DesignSystem/、绝不挂到Theme上、绝不被其他 feature 拿来复用。另一个表面可以把扩展渲染成不透明盒子LauncherScreen对ExtensionArgumentsAccessory正是这么做的但绝不伸进其内部。理由是扩展运行的是不受控的第三方代码所以它绝不能反过来强制改变 launcher 表面的任何东西。为留在隔离区内而复制一个视图或一段布局数学是正确的取舍——这是全项目唯一让禁止重复规则让步的地方。可共享的只有Theme的基础令牌间距、圆角、颜色、作为同一批令牌视图层的InterfaceMetrics、作为数据形态的PopoverMenuItem以及Platform/。ExtensionActionsPanel与ExtensionGridGeometry之所以存在正是因为 palette 自己的菜单和 emoji 网格必须保持自由演进。8.AppEntry.Kind是唯一身份来源AppEntry.Kind是唯一说明一个 entry 是什么的地方每个 launcher section 与每个VisibilityStore类别各一个 case——绝不靠嗅探 entry ID 来重新推导类别。哪个 pane 列出某个命令是另一回事SettingsTab.ownedCommands是唯一陈述它的地方。9. 生成文件永不手编EmojiData.generated.swift←node Scripts/gen-emoji.jsCurrencyData.generated.swift←node Scripts/gen-currencies.jsCountryZoneData.generated.swift←node Scripts/gen-countries.jsResources/RaycastRuntime.generated.js←Scripts/raycast-runtime/build.mjs。运行时文件被提交进仓库所以构建应用永远不需要 Node。docs/development.md#generated-data 补充了细节生成脚本各自下载数据源需联网运行后提交结果gen-currencies.js把汇率 feed 自己的报价列表与 CLDR 数据按 ISO 代码连接——所以币种表与汇率来源永远不会漂移只有无歧义的数据才会被生成有争议的如多个国家共用的dollars、pounds留给CalcCurrency.contested手写。10. 两个滚动文件禁区DesignSystem/Scrolling/EdgeDissolve.swift与ThinScrollbar.swift禁止改动两者都是对着 palette 的悬浮条肉眼调校的任何编辑都是视觉回归。若修滚动 bug 需要动它们说明真正的修复在别处。值得提前知道的约定Conventions类型后缀表后缀说明它是什么AGENTS.md 指向 docs/standards.md#naming 的完整表格。语义正确性永远优先于后缀一致性选能诚实描述类型职责的后缀没有合适时新增一行绝不为凑表而改名。关键条目后缀含义Store拥有持久化状态并发布它RepositoryStore不涵盖的文件语义——冲突检测、修订检查Coordinatorfeature 的动作表面由AppCore与 palette 调用Controller拥有一个 AppKit 窗口或表面Presenter拥有跨表面的呈现策略——单一呈现、自动消失、淡出Manager拥有子系统的生命周期和策略由AppCore.start()启动Service其他类型调用的无状态能力Provider按需提供值不拥有关于其用途的策略Monitor监视外部流并报告变化不拥有策略Scanner读取文件系统以产生候选Runner按请求执行一次有效果的操作Launcher特指NSWorkspace.open包装Center特指 Carbon 注册层Access一个表面的原始平台读取共享以免 walker 意见不一Session一次进行中交互的瞬态状态State自身不持久化任何东西的共享可观察状态Catalog内置列表上的纯静态命名空间Index可搜索集合随输入变化重建Engine纯求值器输入 → 输出Policy纯决策——无状态、无效果注意Manager是唯一需要三思的后缀生命周期 策略负担很重因此全项目只有两个ClipboardManager和HotKeyManager。Registry与ViewModel已被退役静态表是Catalog共享应用状态是State。注释规范罕见、一行、解释 whyAGENTS.md 的规则言简意赅注释罕见、单行、解释为什么gotcha 或 invariant从不解释做了什么。禁止连续两行注释、禁止扩展成块一行写不下就命名一个函数/常量/类型。硬上限 100 字符删除优于更新绝不为你刚做的改动加注释。而且没有任何工具 lint 这条——第一次就要写对docs/standards.md#comments 明确这是刻意选择规则在注释写完后才触发等于买第二次编辑。值得注意的是 docs/development.md#linting 提到 SwiftLint 实际会检查可机器检查的两条100 字符上限与连续注释禁令。Debug 构建是独立渠道Debug 构建运行的是Tinycast Dev.app/com.tinycast.app.dev——本地运行绝不与已安装副本共享 prefs、缓存、TCC 授权或登录项。任何新持久化的东西都必须以Bundle.main.bundleIdentifier为 key。配置证据在 project.yml 的 Debug configPRODUCT_NAME: Tinycast Dev、PRODUCT_BUNDLE_IDENTIFIER: com.tinycast.app.dev。docs/development.md#the-dev-channel 展开说明prefs 在~/Library/Preferences/id.plist数据在~/Library/Application Support/id/可重取的数据才进~/Library/Caches/id/因为 Caches 不进 Time Machine、系统可能在磁盘压力下静默回收。两个已知后果Dev 构建首次会自己申请 Accessibility、默认无热键绑定Hyper Key 的 Caps Lock 重映射是hidutil状态、系统级而非按 bundle退出一个构建会清掉另一个的重映射。XcodeGen 拥有项目Tinycast.xcodeproj被提交但由project.yml生成改完project.yml后运行xcodegen generate并提交两者。不用 SwiftPM永远不用Bundle.module。构建命令docs/development.md#build--runopen Tinycast.xcodeproj # 然后 ⌘R # 或命令行 xcodebuild -project Tinycast.xcodeproj -scheme Tinycast -configuration Debug build若xcode-select指向 Command Line Tools 而非 Xcode需前缀DEVELOPER_DIR/Applications/Xcode.app/Contents/DeveloperSwiftUI 的State/FocusState宏需要 Xcode 的 macOS 平台。Before you finish完成标准Definition of DoneAGENTS.md 的收尾清单指向 docs/testing.md#definition-of-done——机械标准只写在一处所以不会因写了两遍而漂移。五项全部通过才算改动完成检查项命令测试 harness./Scripts/run-tests.shLint./Scripts/lint.sh纯净层纯度grep -rln import AppKit\|import SwiftUI\|import Cocoa Tinycast/Features/*/Model/干净构建xcodebuild … -configuration Debug CODE_SIGNING_ALLOWEDNO零新增警告文档仍然为真你的改动弄错的任何文档在同一提交里修好测试体系没有 XCTest target 的独立 harnessTinycast 刻意没有 XCTest target 与 UI 测试自动化测试是一组独立 harnessTests/下每个 Swift 文件一个。这与纯净层规则形成闭环每个 harness 直接编译它守护的已发布源码而非副本因此harness 停止编译就意味着Model/泄漏了 AppKit/SwiftUI或一个 effect 泄漏进了决策——这比断言失败更常见也更重要。Scripts/run-tests.sh 的机制值得一提运行整个套件./Scripts/run-tests.sh只跑单个./Scripts/run-tests.sh calc-test名字是 harness 名。套件并行执行hw.ncpu个 worker 同时跑TINYCAST_TEST_JOBS1强制串行TINYCAST_TEST_TIMEOUT默认 300 秒超时杀掉卡死的 harness。每个 harness 必须把自己的临时状态根植于专属位置UUID 后缀的temporaryDirectory、UserDefaults(suiteName:)、NSPasteboard.withUniqueName()因为 harness 在真实登录会话、无沙箱、无 fixture 世界里运行绝不能改动与日常使用共享的机器状态——NSPasteboard.general是最大的陷阱正在运行的 Tinycast 会把每次写入都当作真实复制记入剪贴板历史。脚本文件头就记录了run-tests.sh的教训绝不在set -e脚本里用连接编译与运行——set -e规定忽略非末尾 AND-OR 列表成员的失败swiftc … /tmp/x会吞掉编译错误让脚本继续跑历史上有 CI 连续 25 个阶段对根本没编译成功的 harness 报成功。harness 与所守护模块的映射表很庞大docs/testing.md#what-to-run-when例如fuzz-test守护Launcher/Model/SearchRelevance.swift等排序相关文件calc-test守护整个Calculator/Model/raycast-test守护RaycastDecoder、Scrypt、Zlibext-test端到端在 JavaScriptCore 里启动真实 bundle 并渲染。需要服务器的两个 harness 自带 stubTests/ai-fixtures/codex-stub.js、mcp-stub.js被复制进 scratch 目录并前置到 PATH。构建与大小检查docs/testing.md#build-and-size-checks 的机械标准还包括零新增警告存量警告不是你的问题未经说明理由不得新增unchecked Sendable、nonisolated(unsafe)或assumeIsolated类型检查器不得超时LauncherList.rows已带显式注解修超时的方式是加注解而非重构普通改动 Release 二进制增长 2%。性能测量走Platform/Signposts.swift在com.tinycast.perf子系统发射的八个 intervalAppCore.start、AppIndex.scan、AppIndex.rank、PaletteWindowController.show、UninstallScanner.discover、UninstallScanner.measure、FileSearchService.search、Notes.search用 Instruments 的 Time Profiler 或os_signpost过滤到该子系统即可无需重新编译。文档记录的基线2026 重构结束时在main上测得只能当数量级参考而非契约Release 二进制 3,655,736 B、常态驻留内存 40–80 MB硬上限 100 MB、harness 套件约 15 秒墙钟11 路并行串行约 98 秒、改造前约 140 秒。从 AGENTS.md 到四层架构规范如何落地若把 AGENTS.md 视为宪法docs/architecture.md 就是地图。每个成熟子系统都收敛到四层而Tests/的 harness 正是把各层隔开的机制PUREFoundation only环境事实全部注入→ EFFECT平台 I/O→ OBSERVABLE STATEMainActor Observable→ VIEWSwiftUI 视图 coordinator在文件夹树上对应Model/、Service/、UI/Settings/可观察状态住在拥有它的那一层。规则可检查这正是重点Model/不得 import AppKit/SwiftUI因为 harness 编译的是已发布源码。边界把 effect 挡在决策之外CalcEngine.evaluate被交给一个成品CurrencyRates?而不是自己去取——这使它保持 Foundation-only 且可测试。确认门confirmation gate住在 coordinator 而不是 runner 里——这就是为什么ShellCommandRunner与SystemActionRunner能保持可被 harness 编译而你确定吗步骤依然无法绕过。观察层Observation有三个容易踩的坑docs/architecture.md#observation与 docs/standards.md 重复强调memo 缓存与惰性协作对象要加ObservationIgnoredEnvironment上绝不写类型注解宏按类型解析无 key 重载注解会改变所选重载编译器看不见遗漏的注入点——在没人注入的层级里读Environment(AppSettings.self)会编译通过但运行时 trap所以加新 hosting view 时要检查注入。另外withObservationTracking的onChange是willSet 钩子写入落地前触发、一次性所以重读要推迟进Task并在那里重新布防——AppCore.trackAppCore.swift#L599-L612就是该形态的样板。结语把 Tinycast 的规范用起来AGENTS.md 是一份罕见的、可执行的工程宪法它把最新优先、单一所有权、纯净分层、编译级强制与一处定义、五步验收的完成标准绑定成一个自洽体系而其每一条都能在仓库源码、工程配置与测试脚本中找到落实证据。对 Tinycast 的贡献者来说动手前的正确路径是改 wiring 先读 docs/architecture.md写 Swift 先读 docs/standards.md声称完成前跑完 docs/testing.md 的五项检查。对其他 macOS/Swift 项目而言哪怕只移植用 harness 编译正版源码以强制纯净层和单一组合根 无竞争单例这两条也能显著降低代码库长期腐化的速度。【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考