发现于 #3678 的实施(PR #3693)。观察类,不是用户今天会撞到的回归 —— path 是真的,只是 message 泛化。分诊定级请自行裁决,本卡只负责记录测量。
背景
#3626 / PR #3677 引入了一条范畴判定:某个 union 成员如果在 union 节点自身上直接拒绝了值的类型,它压根没读过值,内容就不能作为它的证据,于是它不是候选。#3678 / PR #3693 把同一个判定读成普查:恰好剩一个成员时选它。
两条规则都建立在同一个谓词上:
function memberRejectedNodeType(group: ZodLikeIssue[]): boolean {
return group.some((i) => i.code === 'invalid_type' && (i.path ?? []).length === 0);
}
测量
它只认 invalid_type。Zod 的 enum 成员被喂一个非枚举值时答的是 invalid_value,不是 invalid_type —— 于是这个成员被算作「读过值的候选」,尽管它同样是在节点上整体拒绝、同样没看过内部。
columns[0].summary 是 enum | {type, field},实测(@objectstack/spec,PR #3693 之后):
| body |
成员普查 |
今天的 path + message |
summary: 'bogus'(字符串) |
对象成员答 invalid_type → k=1 |
config.columns.0.summary / Invalid option: expected one of "none"|"count"|… ✅ |
summary: {type: 'bogus'}(对象) |
enum 成员答 invalid_value path=[] → 被计为候选,k=2 |
config.columns.0.summary / Invalid input |
第二行里真正读过这个对象的只有 {type, field} 一个成员;若范畴判定放宽成「任何在 union 节点自身(相对 path 为空)整体拒绝该值的 issue」,k 就是 1,唯一候选规则可以下降到 config.columns.0.summary.type,给出 type 的具名 enum 选项。
为什么 PR #3693 没有顺手改
范畴判定是 #3626 立的,#3678 的有界授权明确是「判定门不动、沿用既有约束」。放宽这个谓词会同时改变两条规则的适用范围(内容判别的弃权集合也随之变化),那是另一次测量 + 另一轮逆向验证,不该搭车。PR #3693 把这一条写进了「已知残留」并在 memberRejectedNodeType 的注释里注明了这个 caveat 是刻意保留的,免得下一个读者以为是漏写。
判断这值不值得做
- 赞成:范畴判定的本意是「这个成员有没有读过值」,
invalid_value at path=[] 同样是「没读过」,只认 invalid_type 是实现细节漏进了语义。放宽之后规则更贴近它自己的说法。
- 反对:要重新普查整个
view 家族确认放宽不会让某个 union 从「无人」掉进「唯一候选」并选错;invalid_value 在 path=[] 上还可能来自 z.literal / refine 等,语义未必都是「整体拒绝」。必须先测量,不能按论证直接改。
- 收益上限很小:目前只影响
summary: {type:…} 这一类形状,且路径已经准确,只是 message 泛化。
倾向:低优先。与 #3626 / #3678 同族,同样在 objectstack#6391(spec 侧提供判别入口)落地后大概率整体消解 —— 若 spec 接了,这一条连同前两条局部规则一起退休。
相关:#3626、#3678、PR #3677、PR #3693、objectstack#6391。
Generated by Claude Code
发现于 #3678 的实施(PR #3693)。观察类,不是用户今天会撞到的回归 —— path 是真的,只是 message 泛化。分诊定级请自行裁决,本卡只负责记录测量。
背景
#3626 / PR #3677 引入了一条范畴判定:某个 union 成员如果在 union 节点自身上直接拒绝了值的类型,它压根没读过值,内容就不能作为它的证据,于是它不是候选。#3678 / PR #3693 把同一个判定读成普查:恰好剩一个成员时选它。
两条规则都建立在同一个谓词上:
测量
它只认
invalid_type。Zod 的 enum 成员被喂一个非枚举值时答的是invalid_value,不是invalid_type—— 于是这个成员被算作「读过值的候选」,尽管它同样是在节点上整体拒绝、同样没看过内部。columns[0].summary是enum | {type, field},实测(@objectstack/spec,PR #3693 之后):summary: 'bogus'(字符串)invalid_type→ k=1config.columns.0.summary/Invalid option: expected one of "none"|"count"|…✅summary: {type: 'bogus'}(对象)invalid_valuepath=[] → 被计为候选,k=2config.columns.0.summary/Invalid input第二行里真正读过这个对象的只有
{type, field}一个成员;若范畴判定放宽成「任何在 union 节点自身(相对 path 为空)整体拒绝该值的 issue」,k 就是 1,唯一候选规则可以下降到config.columns.0.summary.type,给出type的具名 enum 选项。为什么 PR #3693 没有顺手改
范畴判定是 #3626 立的,#3678 的有界授权明确是「判定门不动、沿用既有约束」。放宽这个谓词会同时改变两条规则的适用范围(内容判别的弃权集合也随之变化),那是另一次测量 + 另一轮逆向验证,不该搭车。PR #3693 把这一条写进了「已知残留」并在
memberRejectedNodeType的注释里注明了这个 caveat 是刻意保留的,免得下一个读者以为是漏写。判断这值不值得做
invalid_valueat path=[] 同样是「没读过」,只认invalid_type是实现细节漏进了语义。放宽之后规则更贴近它自己的说法。view家族确认放宽不会让某个 union 从「无人」掉进「唯一候选」并选错;invalid_value在 path=[] 上还可能来自z.literal/refine等,语义未必都是「整体拒绝」。必须先测量,不能按论证直接改。summary: {type:…}这一类形状,且路径已经准确,只是 message 泛化。倾向:低优先。与 #3626 / #3678 同族,同样在 objectstack#6391(spec 侧提供判别入口)落地后大概率整体消解 —— 若 spec 接了,这一条连同前两条局部规则一起退休。
相关:#3626、#3678、PR #3677、PR #3693、objectstack#6391。
Generated by Claude Code