#4001 战役的每一条策展处方,只要它所在的 schema 被包在一个 z.union([...]) 分支里,在线上就是不可见的。测量于 2026-08-03(批 14 收紧 DashboardWidget.compareTo 时,先写断言再证红,发现断言红在了意料之外的地方)。
机制
zod 把一个失配的 union 折叠成一条顶层 invalid_union issue,message 是裸的 Invalid input;各分支的真实错误挂在 issue.errors(每个分支一个数组)。
而 packages/rest/src/rest-server.ts 的 zodIssuesToFields 是:
return issues.map((i: any) => ({
field: ..., code: zodIssueToFieldCode(i, ...), message: String(i?.message ?? 'Invalid value'),
}));
—— 只走顶层。invalid_union 被映射成 code: 'invalid_shape',message: 'Invalid input',issue.errors 里的内容整个丢掉。
实测
DashboardWidgetSchema.parse({ id, dataset, values, compareTo: { offset: '7d', granularity: 'month' } }):
顶层:
code=invalid_union path=["compareTo"] msg=Invalid input
而分支错误里躺着这条(批 14 新写的):
Unrecognized key(s) on this comparison window: `granularity`. Until #4001 closed this shape
these were dropped silently — the dashboard still rendered, without whatever the key was
meant to configure.
作者看到的是 Invalid input。
为什么这是战役级问题而不是一个 widget 的问题
拒绝本身是对的、也是 #4001 的收益(静默半丢弃 -> 硬失败)。问题是处方这一半:账本 finding 18 已经把它写成规则 —— 「prose in a rejection is behaviour, not documentation」。战役花在 aliases/guidance 上的策展,一旦落在 union 分支内,就等于没写;而且没有任何测试会发现,因为对着 schema 断言 message 是能过的(错误确实被产生了,只是没被传递)。
需要一次普查:packages/spec 里有多少条 strictObject( 落在 union 分支内(compareTo、submitBehavior、view.zod 的若干 union 成员等)。
建议
在 zodIssuesToFields 里展开 invalid_union:把每个分支的子 issue 提升为独立的 field 条目(path 需要拼上父 path),或者在只有一个分支「明显是作者意图的那支」时优先提升该分支。后者更难判定但输出更干净 —— 对象输入配对象分支,是一个可用的启发。
⚠️ 这是 wire 契约的改动(fields[] 的条数会变),按 ADR-0114 需要和 mapDataError 保持同形。
批 14 已在 dashboard.zod.ts 的 compareTo 处记下这条实测限制,并在测试里分开 pin 了两件事:作者今天真正看到的裸 message,和等着被传递的处方文案 —— 免得一个绿测试冒充一条没人打印的消息。
发现于 #4001 批 14 的范围外体检,未指派。
#4001 战役的每一条策展处方,只要它所在的 schema 被包在一个
z.union([...])分支里,在线上就是不可见的。测量于 2026-08-03(批 14 收紧DashboardWidget.compareTo时,先写断言再证红,发现断言红在了意料之外的地方)。机制
zod 把一个失配的 union 折叠成一条顶层
invalid_unionissue,message 是裸的Invalid input;各分支的真实错误挂在issue.errors(每个分支一个数组)。而
packages/rest/src/rest-server.ts的zodIssuesToFields是:—— 只走顶层。
invalid_union被映射成code: 'invalid_shape',message: 'Invalid input',issue.errors里的内容整个丢掉。实测
DashboardWidgetSchema.parse({ id, dataset, values, compareTo: { offset: '7d', granularity: 'month' } }):顶层:
而分支错误里躺着这条(批 14 新写的):
作者看到的是
Invalid input。为什么这是战役级问题而不是一个 widget 的问题
拒绝本身是对的、也是 #4001 的收益(静默半丢弃 -> 硬失败)。问题是处方这一半:账本 finding 18 已经把它写成规则 —— 「prose in a rejection is behaviour, not documentation」。战役花在
aliases/guidance上的策展,一旦落在 union 分支内,就等于没写;而且没有任何测试会发现,因为对着 schema 断言 message 是能过的(错误确实被产生了,只是没被传递)。需要一次普查:
packages/spec里有多少条strictObject(落在 union 分支内(compareTo、submitBehavior、view.zod的若干 union 成员等)。建议
在
zodIssuesToFields里展开invalid_union:把每个分支的子 issue 提升为独立的 field 条目(path 需要拼上父 path),或者在只有一个分支「明显是作者意图的那支」时优先提升该分支。后者更难判定但输出更干净 —— 对象输入配对象分支,是一个可用的启发。fields[]的条数会变),按 ADR-0114 需要和mapDataError保持同形。批 14 已在
dashboard.zod.ts的compareTo处记下这条实测限制,并在测试里分开 pin 了两件事:作者今天真正看到的裸 message,和等着被传递的处方文案 —— 免得一个绿测试冒充一条没人打印的消息。发现于 #4001 批 14 的范围外体检,未指派。