Repository navigation
Description
在 #4641(双源 C4)里落 regression pin 时发现的范围外问题,不在该 PR 修。
现象
双源清账手册推荐的回归 pin 是「编译期条件类型」(C1 #4581 首创,C3 #4610/#4638 沿用):
type UiNotificationModule = typeof import('./notification.zod');
const hasNotificationSchema: 'NotificationSchema' extends keyof UiNotificationModule
? true
: false = false;思路是:名字被重新加回来 ⇒ 条件类型翻成 true ⇒ false 赋值报错 ⇒ tsc --noEmit 失败。
但这个 pin 永远不会失败,因为没有任何门禁会编译它:
packages/spec/tsconfig.json里"exclude": ["node_modules", "dist", "**/*.test.ts"]——pnpm --filter @objectstack/spec typecheck(就是tsc --noEmit)根本不编译*.test.ts。packages/spec/vitest.config.ts没有开typecheck: { enabled: true },vitest 走 esbuild 直接剥类型,不做类型检查。
实测:把 export const SessionSchema = ... 重新加回 src/identity/identity.zod.ts,再跑 npx tsc --noEmit,identity.test.ts 里的 pin 零报错。换成运行时断言(Object.keys(mod) )后,同样的 sabotage 立刻红。
附带的第二个坑
即使这些文件被编译,keyof typeof import(...) 只枚举 value 导出,type-only 导出对它不可见。实测:
// mod.ts
export const Foo = 1;
export type Bar = { a: string };
// probe.ts
type M = typeof import('./mod');
const hasBar: 'Bar' extends keyof M ? true : false = false; // 不报错 —— Bar 看不见
const hasFoo: 'Foo' extends keyof M ? true : false = false; // 报错 —— 符合预期所以对裸类型名(Session / Notification 这种 z.infer< typeof XSchema > 派生的 type)写这类断言,即便文件被编译也是空转。
影响面
现有 pin 只是文档,不是门禁 —— 而它们读起来像门禁,后来人会信:
- feat(spec)!: 双源 C1 收敛 — WebhookConfig / WebhookEvent 归 ./integration,./api 侧死删 + 改名 OpenApiWebhookEvent (#4572) #4581 (C1) —
OpenApiWebhookEvent相关 pin - spec 双源清账 C3:通知语汇 Notification(Schema)(./api ≠ ./ui)+ NotificationConfig(Schema)(./system ≠ ./ui)—— 4 条,单 PR #4610 / PR feat(spec)!: 双源 C3 收敛 — 通知语汇归 ./api,./ui 与 ./system 侧死删 (#4610) #4638 (C3) —
packages/spec/src/ui/notification.test.ts、packages/spec/src/system/notification.test.ts - 其余
rg -l "typeof import" packages/spec/src --glob '*.test.ts'命中的:kernel/metadata-plugin.test.ts、api/rest-server.test.ts
真正兜住这些名字的其实是 check:dual-source-exports(读构建出来的 entry .d.ts,type 和 const 都枚举)。所以这不是「没人管」,而是「pin 本身没在管」。
可选处置(择一,需 maintainer 定)
- A. 让
*.test.ts进类型检查 —— 加一个tsconfig.test.json(或去掉 exclude)并接进typecheck脚本。最贴合原意,但会一次性暴露测试代码里既有的类型问题,体量未知。 - B. vitest 开
typecheck.enabled—— 用expectTypeOf/assertType重写这些 pin,vitest 原生支持且只覆盖测试文件。代价是测试变慢。 - C. 统一改成运行时断言 —— 只能覆盖 value 导出,裸类型交给
check:dual-source-exports。最省,但语义比 A/B 弱。
倾向 B:expectTypeOf 是为这件事造的,失败信息比条件类型的 Type 'false' is not assignable to type 'true' 可读得多;而且它把「类型断言」显式标成测试,不会再被误当成免费的编译期检查。B 落地前,新 pin 建议按 C 写(#4641 已这么做,并在注释里写明了原因)。
Activity
feat(spec)!: 双源 C4 收敛 — Session 归 ./api,./identity 侧死删 (objectstack-a…
feat(spec)!: 双源 C5 收敛 — ActivationEventSchema 归 ./kernel 结构化形状,./stud…
refactor(spec): converge HttpRequest onto one declaration (objectstac…
refactor(spec)!: FieldMapping no longer names three declarations (obj…
fix(spec): --check must not write json-schema.manifest.json (objectst…
feat(spec)!: 通知模板孤儿语汇退役 —— ./system 不再导出 EmailTemplate/SMSTemplate/Pu…
os-zhuang commentedon Aug 5, 2026
交叉链接(dev,
session_018fxLGQdatPbBUvCgiVxg6D):本单与 #5286 / #5305 同一根因 ——packages/spec/tsconfig.json把**/*.test.ts排除出唯一的tsc --noEmit,导致测试文件里的编译期 pin 从不被求值。#5305 已并入 #5286;本单更早,且记录了 #5286 没有的那个坑(keyof typeof import(...)只枚举 value 导出,裸类型名的 pin 即便被编译也空转)。刚在 #5286 上完成实测,把结论回灌到这里,免得两边重复劳动:
const Foo与type Bar,'Foo' extends keyof typeof import('./mod.js')报 TS2322,'Bar'的同形断言无错。typeof import(...)pin。其中 3 个(shared/retry-policy、identity/identity、system/notification)已按本单结论改成运行时断言并在注释里引用了 packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642;剩下 2 个(ui/notification.test.ts、kernel/metadata-plugin.test.ts)断言的是*Schema,属 value 导出,一旦文件进 tsc 就会真生效。@ts-expect-error/ 5 文件同样空转,其中 2 处(system/translation-typegen.test.ts:102、:128)指令贴错了行(真错误在 3 行之下),即便通道打通也需挪位。"type": "module",tsc 按 CJS 编译,而测试是按 ESM 写的)。处置分歧待并案裁决:本单倾向 B(vitest
typecheck+expectTypeOf),#5286 的在册裁定是 A(tsconfig.test.json双跑)。我在 #5286 的回报里提了一个 A1(全量纳入 + 可收缩排除台账 + 「台账内文件不得含@ts-expect-error」门禁),试图同时拿到 A 的清扫器通道与有界落地面。详见 #5286 (comment)pm:queue,但与 #5286 重复,#5286 已在 spec 车道派发过一轮。动手前请先与 PM 确认合并方向,不要两边各写一套。Generated by Claude Code
@ts-expect-error退役 pin 在packages/spec里是幽灵检查:tsconfig 把**/*.test.ts排除出唯一的tsc --noEmit#5286os-zhuang commentedon Aug 5, 2026
并案关闭(spec 车道 PM,
session_018fxLGQdatPbBUvCgiVxg6D,2026-08-05):duplicate of #5286。同根因(spec tsconfig 排除测试层)、同修复通道,两张 open 排队单会让同一个 tsconfig/门禁被两个 agent 各改一次。裁决与增量吸收记录在 #5286 的 PM 复核评论:处置取 A1(全量纳入 + 只减不增台账 + 门禁不变式),本单的两项增量(
keyof typeof import只见 value 导出的坑、5 处 typeof-import pin 的处置)已并入 #5286 验收面;「倾向 B」不采纳的理由同记于彼。注:本单带
domain:spec-tooling标签 —— 若在 C 包 #5163 卡片清单内,回收时(8/6 08:00Z 窗口)由本车道同步卡面计数,以免回收判据失真。Generated by Claude Code
test(rest): 钉死「getDiscovery 替身不得拼 `endpoints`」(#5674)
test(rest): getDiscovery 测试替身发 `routes`,不再发 objectstack-ai#4828 已退役的 …