shim 审计与同步
"缺失再导出"是一类会反复回来的 bug。ttsc 用 packages/ttsc/tools/shim_audit 把它变成 CI 可强制的不变量。本页讲三层审计闸门、gen_shims 生成器、版本 bump 流程、以及一类审计看不见的"运行时死胡同"探针。内容主要来自 .codex/skills/typescript-go-sync/SKILL.md 与 packages/ttsc/tools/。
机械完整性闸门
CI 的 shim-audit job 跑 pnpm --filter ttsc shim:audit。审计把 shim 当作一个闭包:如果一个类型被别名了,从它可达的一切都应能通过 shim 触达。分三层:
TIER-1:枚举家族(零容忍)
shim/<pkg>/enums_gen.go 补全每个暴露的枚举家族——再导出任何还没在该包 shim.go/surface.go 里出现的成员。闸门对任何部分枚举失败。tsgo bump 后跑 pnpm --filter ttsc shim:audit -fix 重新生成它。这就是 SignatureKindConstruct(#230)那一类的防线:以前少导出一个枚举成员,插件就拿不到它。
TIER-2/3:可达函数 / 逃逸类型(棘轮 ratchet)
tools/shim_audit/baseline.json grandfather 当前 backlog;闸门对任何新 gap 失败。要么暴露符号,要么跑 pnpm --filter ttsc shim:audit -write-baseline 故意接受它。baseline 里的条目形如 ESCAPE|checker|ConditionalType、ESCAPE|checker|EmitResolver——这些是 checker 包里"逃逸"了但尚未再导出的类型,被显式接受为已知缺口。
棘轮的意义:不要求一次性补全所有历史缺口(那会阻塞一切),但保证缺口不再增加——每个新增缺口要么修,要么显式记账。
TIER-3:未导出辅助(需求池)
闭包预测不了这些(一个新消费者第一次要某个内部 helper)。审计把它们列成需求池。用 //go:linkname 暴露(Checker_getMinArgumentCount 是工作例子)。
gen_shims 生成器
packages/ttsc/tools/gen_shims 从 packages/ttsc 跑 go run ./tools/gen_shims,对每个 shim 子模块:
- 读对应
internal/<pkg>的导出 API; - 生成
surface.go(类型别名); - 跳过首行带
// gen_shims:hand-maintained的文件(shim.go等); - 消费
extra-shim.json的ExtraFunctions/ExtraMethods/ExtraFields/IgnoreFunctions,处理自己推不出的符号。
生成器是确定性的:重跑覆盖 surface.go,但永远不碰手写文件。
版本 bump 流程
完整步骤(.codex/skills/typescript-go-sync/SKILL.md):
- 把
require github.com/microsoft/typescript-go v0.0.0-<时间戳>-<hash>更新到新伪版本——每个shim/*/go.mod都改、保持一致,packages/ttsc/go.mod也改,再刷新各go.sum。 - 从
packages/ttsc重跑go run ./tools/gen_shims对新源重新生成surface.go。 - 复查手写
shim.go与extra-shim.json:上游 rename、签名变更、导出/未导出翻转都可能破坏一个包装或 linkname。构建packages/ttsc并修 fallout。 - 跑
pnpm --filter ttsc shim:audit -fix重新生成枚举补全。
在真实消费者里验证
一次 shim 变更只有当下游插件能编译并通过测试才算证实。流程:
把产出的 tarball 装进消费者 checkout(如 ../typia),跑一个会触碰新 API 的 typia 测试。experimental/tarballs/index.ts 流程就是 CI 用的;--current / TTSC_TARBALLS_CURRENT=1 只打当前平台包做快速循环。
遍历完整性探针(审计看不见的一类)
闭包与审计只能看到一个符号是否可命名、一个组合是否能编译。它们看不到运行时死胡同:一个暴露的图遍历操作静默地无法触达图的某部分。
例子(#246):Checker_getBaseTypes 在泛型 Reference 基类型上 nil-deref,于是 base-chain 遍历在泛型边界处死胡同——审计看不见,只在消费者崩溃时浮现。
对策是运行时探针:在 ttsc 自己的夹具上跑暴露的遍历,断言它能完成。工作例子 packages/lint/test/shim/base_chain_walk_crosses_generic_boundary_test.go:经 lint 宿主的 loadProgram 建一个真实 Checker,沿 Base{#brand} <- Mid<T> <- Sub extends Mid<string> 链只用暴露的 shim 操作遍历,断言朴素遍历在到 Base 前死胡同(缺口真实存在)、而经 getDeclaredTypeOfSymbol 桥接的遍历能到达。这些测试住在 packages/lint/test/(唯一拥有 over-source Checker 的 ttsc Go 测试套件),由 pnpm test:go 跑。
维护者规则:暴露一个新图遍历操作时,加一个夹具 + 完整性断言,让它的死胡同不能静默回来——优先这个,而非只验证链接的 compile-only 守卫(后者证明链接但不证明遍历)。
闸门总结表
维护者提示
- 别手改
surface.go/enums_gen.go——下次gen_shims会覆盖。改extra-shim.json或手写shim.go。 - baseline 只该用
-write-baseline改,不要手编辑,免得记账漂移。 - tsgo bump 后,按 1→2→3→4 顺序走,且最后一步(枚举
-fix)别忘。