gstack iOS 调试桥再同步:/ios-sync 技能如何确定性重生成 DebugBridge 与状态访问器
2026/9/7 6:13:13 网站建设 项目流程

gstack iOS 调试桥再同步:/ios-sync 技能如何确定性重生成 DebugBridge 与状态访问器

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

gstack 的 iOS QA 能力会在目标 app 内安装一个仅 DEBUG 生效的调试桥(DebugBridgeSwift 包 + 生成的StateAccessor.swift)。当 app 新增了需要快照的状态字段、或 gstack 本身升级到带加固修复的新版本后,这些已安装文件就会与上游模板漂移。本文围绕 ios-sync 技能文档 展开:它讲解/ios-sync何时被触发、按哪四个阶段完成再同步,以及底层 gstack-ios-qa-regen 启动器 和 gen-accessors 生成器 如何用模板白名单、@Snapshotable标记契约和复合哈希缓存,做到字节级可重复、失败即显式的确定性重生成。

1. 背景与触发时机

/ios-sync的定位是 /ios-qa 的后续维护工具:/ios-qa负责首次安装调试桥,/ios-sync负责把已安装的桥文件重新对齐到最新版上游 gstack 模板,/ios-clean则负责发版前拆除。技能文档给出的三个典型触发场景:

  1. 你新增了@Observable类或字段,需要访问器覆盖(accessor coverage);
  2. 你把 gstack 升级到带有加固修复的新版本;
  3. 你把// @Snapshotable生成器标记注释移动到了别的字段上。

技能 frontmatter 里声明的触发语(triggers / voice triggers)为:resync the ios debug bridgeregenerate iOS accessorsupdate the gstack iOS instrumentation

一个关键设计前提写在文档中:模板存放在上游 gstack 里。已安装到 app 中的gstack-ios-qa-regen启动器会自己解析 gstack 根目录,只从ios-qa/templates/拷贝受支持的桥文件;旧分支(fork)里"从 HTTP 抓取模板 + 通配符拷贝"的模式已被移除。这意味着再同步永远以本地 gstack 安装为单一事实源,不会引入网络依赖。

2. 四阶段工作流总览

技能正文从 Preamble 执行完之后开始,分四个阶段:

  • Phase 1:检测已安装版本——对比 app 内的版本标记与上游 VERSION,无变化则提前退出;
  • Phase 2:重新生成 codegen 产物——运行确定性的再生成器;
  • Phase 3:审查生成物 diff——确认没有误伤手写文件;
  • Phase 4:验证——构建、真机重连、快照 schema hash 校验。

下面逐阶段展开,并对照仓库源码说明每一步的底层保证。

3. Phase 1:检测已安装版本

  1. 读取<app>/DebugBridgeGenerated/.gstack-version(该文件由/ios-qa安装时写入)。文件缺失时,把安装视为"未知旧版本"。
  2. 读取上游版本$GSTACK_ROOT/VERSION
  3. 若版本一致且没有新增@Observable类,直接以 "already up to date" 提前退出。

这个.gstack-version文件是整个再同步流程的完成标记:启动器在改动任何包源码之前会先删除它,只在访问器生成成功之后才重新写入。因此"标记存在"等价于"这是一次完整且与当前 gstack 版本对齐的安装",而"标记缺失"则说明上次生成被中断或失败,下次/ios-sync必须完整重跑。

4. Phase 2:运行确定性再生成器

文档给出的标准调用:

~/.claude/skills/gstack/bin/gstack-ios-qa-regen \ --app-source "$APP_SOURCE_DIR" \ --bridge-dir "$APP_SOURCE_DIR/DebugBridge"

参数语义:

  • --app-source:访问器扫描器应检查的 Swift 源码目录(建议传尽量窄的 app 源码目录,减少扫描面);
  • --bridge-dir:app 在 Debug 构建中链接的本地 Swift 包目录。

4.1 启动器做了什么

bstack-ios-qa-regen 是一个 bash 脚本(set -euo pipefail),行为可以从源码逐段确认:

  1. 参数校验--app-source--bridge-dir缺一即报错并退出码 2;未知参数同样退出 2。测试 专门钉死了这个契约:缺少--bridge-dir时 stderr 必须包含both --app-source and --bridge-dir are required
  2. 环境检查$APP_SOURCE必须是存在的目录(否则退出 1);bun必须在 PATH 上(生成器是 TypeScript,由 bun 运行)。
  3. 解析 gstack 根:启动器取自身所在目录的上级作为$GSTACK_ROOT,随后定位三个关键资源:
资源路径用途
生成器ios-qa/scripts/gen-accessors.ts扫描源码、写出 StateAccessor.swift
模板目录ios-qa/templates/规范桥文件
版本文件VERSION写完成标记

两个必需文件缺失时退出 1。这也解释了文档中"启动器解析自己的 gstack 根"的含义——它只依赖脚本自身的安装位置,不依赖当前工作目录。 4.先失效完成标记:在拷贝任何文件之前执行rm -f -- "$GENERATED_DIR/.gstack-version",注释写明"失败或被中断的再生成绝不能对 ios-sync 伪装成最新状态"。测试 用一个伪造的、以退出码 17 失败的bun验证了这一点:生成失败后.gstack-version必须不存在(连之前遗留的旧标记也会被清掉)。 5.白名单拷贝 7 个规范包文件(见 4.2)。 6.删除旧版平铺布局遗留文件(见 4.3)。 7.运行生成器bun run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"。 8.成功后才打标记:把$GSTACK_ROOT/VERSION的内容装进DebugBridgeGenerated/.gstack-version。 9. 打印两行就绪信息:bridge 包位置与 accessors 位置。

拷贝用的install_file函数有两个值得注意的细节:目标文件已存在且与源模板逐字节一致(cmp -s)时直接返回,不碰文件;只有内容变化时才经由destination.tmp.$$临时文件 +mv原子替换。注释说明这保证"中断永远不会留下截断的生成源码",且重复运行时未变化的文件字节与元数据都保持不变——这正是测试里"二次运行树哈希不变"断言的来源。

4.2 模板白名单:7 个规范文件,刻意不用通配符

启动器中的注释明确写道"这是白名单,不是模板 glob",因为它要把 app 自己拥有的接线文件排除在生成范围之外。安装映射与 测试中的 SAFE_TEMPLATE_MAP 完全一致:

模板(ios-qa/templates/)安装到($BRIDGE_DIR 下)
Package.swift.templatePackage.swift
StateServer.swift.templateSources/DebugBridgeCore/StateServer.swift
DebugBridgeManager.swift.templateSources/DebugBridgeCore/DebugBridgeManager.swift
Bridges.swift.templateSources/DebugBridgeUI/Bridges.swift
DebugOverlay.swift.templateSources/DebugBridgeUI/DebugOverlay.swift
DebugBridgeTouch.m.templateSources/DebugBridgeTouch/DebugBridgeTouch.m
DebugBridgeTouch.h.templateSources/DebugBridgeTouch/include/DebugBridgeTouch.h

注意StateAccessor.swift在拷贝列表里——它由生成器解析源码后产出,不是模板。测试里专门往伪造模板目录塞了带FORBIDDEN-WIRING-SENTINEL/FORBIDDEN-STATE-SENTINEL内容的DebugBridgeWiring.swift.templateStateAccessor.swift.template,断言它们绝不逃进 app;如果启动器哪天回归成通配符拷贝,这个哨兵内容就会泄漏并使断言失败。

生成后的包由三个 SwiftPM target 组成,可用swift package dump-package验证 target 集合为DebugBridgeCoreDebugBridgeTouchDebugBridgeUI(测试中的校验 在环境存在 swift 时执行)。其中StateServer是嵌入 app 的 loopback-only HTTP 服务(默认端口 9999,#if DEBUG门控,boot token 在 daemon 启动后约 5 秒内轮转),完整架构见 iOS 测试 howto。

4.3 旧版平铺布局的定点清理

更老版本的 ios-sync 会把整组模板平铺进 app 的生成目录。那些文件会遮蔽(shadow)新包模块,或让 Xcode 编译出第二套过时的桥实现。启动器因此维护一份显式废弃清单(源码中的 obsolete 循环),逐项删除以下 10 个路径,绝不使用通配符,绝不动手写文件:

  • $BRIDGE_DIR/DebugBridgeWiring.swift
  • $BRIDGE_DIR/StateAccessor.swift
  • $GENERATED_DIR/Package.swift
  • $GENERATED_DIR/StateServer.swift
  • $GENERATED_DIR/DebugBridgeManager.swift
  • $GENERATED_DIR/Bridges.swift
  • $GENERATED_DIR/DebugOverlay.swift
  • $GENERATED_DIR/DebugBridgeTouch.m
  • $GENERATED_DIR/DebugBridgeTouch.h
  • $GENERATED_DIR/DebugBridgeWiring.swift

这与技能文档"命令只删除已知过时的生成文件"一一对应。

5.@Snapshotable标记契约:生成器的准入规则

生成器 gen-accessors.ts 是一个轻量 Swift 词法扫描器(TS 快速路径,避免首次构建 swift-syntax 工具链的 2-5 分钟等待)。它的行为边界就是技能文档里"Generation accepts … rejects …"那句话的完整展开。

5.1 标记如何被识别

扫描前先做一遍"掩码"(maskSwiftSource):把注释与字符串字面量整体空白化,但保持字节偏移与换行不变;同时只把独立的// @Snapshotable注释行重写为等价的@Snapshotable属性 token。注释强调这是刻意选择的词法级处理而非全文件正则:嵌套块注释、普通/三引号/原始字符串、行尾注释和正文注释里的@Snapshotable都绝不能让字段"被标记"。

5.2 字段准入条件(生成器会硬性拒绝)

  • 必须是文件作用域@Observable类的可写实例var,带显式类型注解,internal 或 public setter;
  • 拒绝let("must be declared var, not let")、private/fileprivate/private(set)/fileprivate(set)static/class属性;
  • 拒绝无类型注解、计算属性、多绑定声明;
  • 类型只接受JSON 原生类型:标量(StringBool、各符号/无符号整型宽度、FloatDoubleCGFloat)、数组、String 键字典,以及这些类型的Optional组合;
  • 拒绝自定义类型、隐式解包OptionalT!,提示"用T?代替")、嵌套 Optional、非 String 键字典。

文档中"在写完成标记之前拒绝无效声明"对应的实现是:以上任何违规都会累积进诊断信息,最终以AccessorGenerationError抛出(CLI 退出码 4),而不会产出到 xcodebuild 或快照时才失败的坏 Swift。另外,多个@Observable类之间的字段 key 必须全局唯一,冲突同样直接报错(snapshot key '<name>' is declared by both A and B)。

5.3 生成物形态

render()为每个@Observable类生成一个@MainActor<ClassName>Accessor枚举,注册buildIdaccessorHash与两阶段atomicRestore处理器,并为每个标记字段注册 read/write 访问器。有两处细节体现"app 为中心"的设计:

  • 生成的枚举是非 public的。源码注释解释:访问器编译在 app target 内,与通常是 internal 的状态类型同居;若把 API 声明为 public,public 签名就会暴露 internal 类型,导致 Swift 类型检查直接失败。测试也断言产物"包含enum AppStateAccessor但不包含public enum AppStateAccessor"。
  • atomicRestore是两阶段的:先校验全部 key 与值类型,再按序赋值;apply为真时才真正写状态。这与 StateServer 模板 中的注释一致——"服务器先验证所有模型,才允许任何模型被修改,非法输入永远不能造成跨模型的部分恢复"。

6. 复合哈希缓存与 schema hash:~50 毫秒的空操作

技能文档说:"复合哈希缓存键决定了是否真的需要重生成;若 Swift 版本、生成器 git rev、lockfile、源码内容与平台三元组全部命中缓存,这就是一次约 50ms 的空操作。"对照 computeCacheKey 的实现,缓存键是 SHA-256,输入依次为:

  • 生成器格式版本(当前为accessor-generator-v5);
  • swift=<Swift 版本>(取SWIFT_VERSION环境变量或swift --version,探测失败记unknown);
  • tool=<生成器 git rev>GEN_ACCESSORS_REV环境变量或 gstack 仓库的短 SHA,非 git 环境记dev);
  • platform=<平台三元组>(darwin 下为darwin-arm64);
  • build=<构建 ID>APP_BUILD_ID/MARKETING_VERSION+CURRENT_PROJECT_VERSION,否则unknown);
  • 每个 Swift 源文件的字节数 + 内容

两个设计点值得强调:源码文件按排序后的相对集合遍历,绝对 checkout 路径不参与哈希,等价源码树在任何机器上都产出同一缓存键;扫描时自动排除DebugBridgeGenerated目录与任何位置的StateAccessor.swift(否则移动一份旧副本就会污染下一次缓存键)。

缓存根目录默认为~/.gstack/cache/gen-accessors,可用GSTACK_IOS_CACHE_ROOT覆盖(defaultCacheRoot);命中时直接把缓存里的StateAccessor.swift拷回输出目录。缓存另有 30 天自动清理(pruneCache)。

与缓存键不同,schema hashaccessorHash)由 computeAccessorHash 计算:对snapshot-schema-v1签名逐类逐字段写入类名、字段名、类型文本(均带字节长度前缀,保持源码顺序),再取 SHA-256。注释说明它刻意独立于缓存 ABI、源码路径、构建来源与未标记源码——"字段/类顺序就是源码顺序,因为 restore 载荷兼容性是一个有序的契约"。这个 hash 会被打进生成的StateAccessor.swift并注册进StateServer,Phase 4 用它验证真机上的/state/snapshot是否反映了新 schema。

7. Phase 3:审查生成 diff

文档要求:

  1. 检查<app>/DebugBridge/下的改动,以及<app>/DebugBridgeGenerated/StateAccessor.swift>
  2. 确认命令没有修改 app 的手写 Swift 文件——启动器只写白名单内的 7 个路径加生成目录里的StateAccessor.swift.gstack-version,其余文件不可能被动到;
  3. app 特定的接线(wiring)留在 app target 里;规范的桥包文件一律由上游重新生成,不手工编辑

仓库测试为此提供了可验证的基线:安装后的每个包文件必须与其源模板逐字节一致("持久的模板/输出一致性契约"),且整个 bridge + generated 目录的树哈希在二次运行后不变、accessorHash不变、stderr 为空——这就是"确定性、幂等"的机器化定义。

8. Phase 4:验证

技能文档列出的四条验证,逐条对应仓库中的可检查事实:

  1. swift build对 app 的包成功——DebugBridgeCore是跨平台 target,CI 的 Mac 主机上不用 UIKit 就能验证包的大部分代码;
  2. xcodebuild -scheme <SchemeName>成功;
  3. 在真机重启 app;daemon 重连并完成 token 轮转;
  4. GET /state/snapshot返回新的accessor schema hash——即第 6 节中计算并注册进StateServer的那个值,用它确认真机跑的是新访问器而非旧缓存状态。

9. 失败模式与处理

文档给出的失败模式表(原文完整继承):

症状处理
regen 后 Swift 编译失败git restore回滚,并通过 AskUserQuestion 展示编译错误
codegen 报告标记声明无效使用文件作用域 observable 类 + 带显式 JSON 原生类型的可写实例var,internal/public setter,key 跨模型唯一;否则移除// @Snapshotable标记
新增@Observable后 schema hash 没变没有字段带独立的// @Snapshotable标记注释——codegen 正确排除了未标记状态。把注释加到每个需要快照的字段正上方
扫描器看到了生成的桥源码传入更窄的 app 源码目录;再生成器本身会自动排除DebugBridgeGeneratedStateAccessor.swift

第三行对应"漏标记"这一最常见的软故障:未标记字段从不进入快照,这是刻意设计(避免 token、PII、鉴权状态默认流入录制 fixture,见 howto 文档 中的示例),所以"hash 没变"不是 bug 而是提示你忘了标记。

10. 共享 Preamble、计划模式与安全操作

ios-sync/SKILL.md中篇幅最大的 Preamble 是 gstack 所有技能共用的环境探测脚本(由 SKILL.md.tmpl 经bun run gen:skill-docs生成),要点:

  • 探测版本更新(UPGRADE_AVAILABLE/JUST_UPGRADED)、分支、会话类型(spawned/headless/interactive)、Conductor 会话、遥测与explain_level配置,并写入本地 timeline 与 learnings 加载;
  • 计划模式安全操作:计划模式下允许执行会"为计划提供信息"的操作——$B$Dcodex exec/codex review、对~/.gstack/的写入、对计划文件的写入、open生成产物;技能调用优先于通用计划模式行为,技能文件被视为可执行指令逐步遵循;
  • Conductor 会话下 AskUserQuestion 不可靠,决策改为以 prose 形式呈现,而非调用工具。

/ios-sync这类运维型技能来说,Preamble 的意义是:无论交互、无头还是编排器拉起的会话,再生成都遵循同一套"先探测环境、失败即 BLOCKED、绝不静默自动决策"的纪律。

11. 测试如何钉死这套契约

test/ios-qa-regen.test.ts 用伪造安装(临时目录里的bin/gstack-ios-qa-regen、模板与 VERSION9.8.7.6)在沙箱中真实执行启动器,覆盖五类断言:

  • 启动器可执行(mode & 0o111);
  • 参数契约:缺参退出码 2 且 stderr 含指定文案;
  • 生成失败(伪造 bun 退出 17)后完成标记必须不存在;
  • 幂等再生成:安装 7 文件逐字节等于模板、废弃清单全部清除、哨兵内容不泄漏、.gstack-version等于安装 VERSION、二次运行输出gen-accessors: cache hit且树哈希与accessorHash不变;
  • 有 Swift 工具链时swift package dump-package验证 target 集合恰为三个规范模块。

另有 gen-accessors.test.ts 在无 Swift 工具链的环境下验证解析与缓存行为。

12. 小结:何时运行 /ios-sync,如何确认成功

把技能文档与源码合起来,/ios-sync的可执行心智模型是:

  1. 触发:新增需要快照的@Observable状态、升级 gstack、或移动了// @Snapshotable标记之后,用 "resync the iOS debug bridge" 触发技能。
  2. 核心命令~/.claude/skills/gstack/bin/gstack-ios-qa-regen --app-source <app源码目录> --bridge-dir <app>/DebugBridge;它先清完成标记、白名单装 7 个模板、定点删 10 个旧文件、跑生成器、成功后打.gstack-version标记。
  3. 成功信号swift buildxcodebuild通过;真机重启后 daemon 重连并轮转 token;GET /state/snapshot返回新的accessorHash;手写 Swift 文件零改动;.gstack-version内容等于 gstack 安装的VERSION
  4. 失败兜底:编译失败就git restore回滚并展示错误;codegen 拒绝无效标记时按诊断修正声明或移除标记——"完成标记只在成功时写入"保证下一次/ios-sync永远不会把一次中断的安装误判为最新。

整条链路的设计目标是:调试桥的每一次再生成都可被测试复现、可被哈希验证、失败时显式可见——这正是"确定性重生成"在 gstack iOS QA 栈里的含义。

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询