Repository navigation
formatZodError 把 union 分支的拒绝信息压成 "Invalid input" —— #4001 策展的散文在 CLI 路径上到不了作者 #4971
Description
Activity
- added and removed
on Aug 3, 2026 认领:PM 循环第 1 轮(v17 ci-tooling 工作集 / C 包 #5163)
会话:session_01FTszibd6C8sUCCZnM4VcrL
分支:claude/issue-4971-union-error-prose
Worktree:objectstack-issue-4971
域:domain:spec-tooling
文件面:packages/spec/src/shared/error-map.zod.ts(formatZodError 展开invalid_union)、packages/spec/src/automation/state-machine.test.ts(CONTROL pin 翻转)、断言错误散文的相关 pin 测试、.changeset/*⚠️ 触packages/spec/src/**zod 文件 —— 已按 #5163 卡片规则在卡上留言与 spec 车道协调(与其在飞面无重叠)。
⛔ 本单不碰shared/suggestions.zod.ts(#4990 因同 touchstate-machine.test.ts与本单串行,下轮派发)、不碰packages/rest/**(同族 #5014 另单处理)。
Generated by Claude Code
PM 复核(第 1 轮):ACCEPT → PR #5342
会话
session_01FTszibd6C8sUCCZnM4VcrL。⚠️ 首先记一条本单正文的前提错误(dev 证伪,PM 独立复核确认)本 issue 正文写的「
formatZodError的文档用途正是 CLI 输出(os validate/os compile)」是错的。 dev 在实施中发现并更正,我按「不取报告自述」的规矩独立核验origin/main:packages/cli/src/utils/format.ts:167: export function formatZodErrors(error: ZodError) ← CLI 自己那份 packages/cli/src/commands/validate.ts:101: formatZodErrors(result.error as unknown as ZodError); packages/cli/src/commands/compile.ts:162: formatZodErrors(result.error as unknown as ZodError);两个命令调的是 CLI 本地的
formatZodErrors(复数),不经过 spec 的formatZodError。JSDoc 那句话是历史遗留,不是事实。已立 #5341 承接 CLI 那一半,并入队(domain:cli,target:v17)。本条更正留在此处,免得下一个读本 issue 正文的人继承这个错误前提。这不影响本单结论,dev 也给出了修复非空转的证据:
defineStack的严格模式抛错走的正是formatZodError(stack.zod.ts:1250),任何加载 stack 配置的命令都会执行它。升级体验这条线的真实形状因此需要更新为三个消费者、三份独立代码:
消费者 文件 状态 formatZodError(spec 公开导出,defineStack)packages/spec/src/shared/error-map.zod.ts本单已修 zodIssuesToFields(REST wire)packages/rest/src/rest-server.ts#5014 待修 formatZodErrors(CLI 终端,作者实际撞上的那条)packages/cli/src/utils/format.ts#5341 待修 通过项
- 先证后改:改前实测
ActionRefSchema的invalid_unionpayload 结构与✗ (root): Invalid input输出,与正文所述一致。 - 分支是「挑」的不是「倒」的 —— 本单唯一必须避免的失败模式(未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 批 6c:一个未知键被 N 个分支各报一次,
submitBehavior因此改用discriminatedUnion)有专门的钉reports one unknown key ONCE, not once per branch守着。选择规则(丢弃只报「值的种类不对」的分支 → issue 最少者胜 →unrecognized_keys破平局 → 声明顺序兜底)有理有据:作者真正在写的那个成员只抱怨那一个多余的键,这是机制本身而非事后去重补丁。 - 保守的那一半同样被钉住:全部分支都是种类不匹配时(
z.union([z.string(), z.number()])收到对象)完全不展开,输出与改前逐字节相同并有toBe(...)精确断言。原始类型 union 是全仓最常见的 union,这个取舍正确。 - 表头计数仍取
error.issues.length—— CLI 与 REST 错误体、ZodError.message对「错了几处」保持同一口径,这一点单独有钉。 - 测试证据诚实:9 条新钉里 6 条改前必红,另外 3 条两边都绿并主动说明是设计如此(钉的是「不许变」的不变量),没有凑成「全红转全绿」。批 10 预言的 CONTROL 钉按其自述翻转,且保留了反空洞性质。
- 消费半径普查到位:本仓唯一非测试调用点
stack.zod.ts:1250,objectui / cloud 两仓零引用。 state-machine.zod.ts的仅注释越界改动批准 —— 该 JSDoc 原文说「formatZodError是丢弃者之一,filed 而非在此修」,本 PR 之后这句是假的,留着会误导下一个读者。与 AREA_REQUIRED_PERMISSIONS_RETIRED 的处方在 #4722 之后过时:仍写着「the server does not walkareas」 #4749 同一判断。- changeset 定
minor合理。PM 补充核验:仓库当前在 changeset pre 模式(.changeset/pre.jsonmode=pre / tag=rc,spec 已17.0.0-rc.2),major 已累积并支配 minor,GA 版本号不受影响,只递增 rc 序号。 - 范围合规:未手改 strictness 台账(dev 正确识别该文件的
state-machine.zod.ts行现已过时,但那是 spec 车道独占面,留给该车道 —— 判断正确);⛔content/docs/releases/零改动;未碰packages/rest/**。
已知限界(dev 如实记录,不作返工项)
深度上限 3 层时,最内层的处方不会到达作者(
stops expanting after three levels那条钉直接断言not.toContain('bogus'))。当前仓内实际最深两层(StateMachine → on.GO → actions[0]),留有余量;把它写成断言而不是藏起来是对的。后续
- 未知键的「你是不是想写」兜底对 camelCase 键系统性偏弱:输入被小写化、候选没有 —— 每个大写字母白扣一次编辑距离 #4990 解锁条件:本 PR 合入 main 后即可派(两单同 touch
state-machine.test.ts)。 - CI 全绿后转 ready 入队。
Generated by Claude Code
- 先证后改:改前实测
复核补记(我的 ACCEPT 之后 PR 又推了三个提交,如实记录差异):
我发出 ACCEPT 时,PR 的 changeset 里仍然写着那句被证伪的前提——「
formatZodErroris documented for, and used on, the CLI path (os validate/os compile)」。我复核了正文与代码,却没抓到这句话还留在 changeset 里。dev 随后自己发现并推了0810656(连同1a7f283/25a14a8两处注释),改成真实到达面:…What dropped it is every consumer that flattens to one line, and
formatZodErroris one: it is this package's documented CLI formatter and whatdefineStackthrows through, so an author whose stack config has a union mistake reads it on every command that loads the stack.这条要紧在哪:changeset 是发布说明的输入,那句话会随 v17 发出去。一个已被本单实测推翻的前提,差点以「已发布事实」的身份进入客户读的文档 —— 而它错的方向恰恰会让读者以为
os validate的 union 报错已经修好了(真正修那条路径的是 #5341,尚未动工)。判决不变:ACCEPT,且比我复核时更强。三个新提交均为文案/注释,零逻辑改动。
Generated by Claude Code
已落地 —— PR #5342 经合并队列合入 main(
546ab3c49)会话
session_01FTszibd6C8sUCCZnM4VcrL。落地后复核判据本身(队列合并同样走 os-regen 驱动):$ git grep -c "selectUnionBranches" origin/main -- packages/spec/src/shared/error-map.zod.ts 3 $ git grep -c "invalid_union" origin/main -- packages/spec/src/shared/error-map.zod.ts 7 $ git grep -c "this action reference" origin/main -- packages/spec/src/automation/state-machine.test.ts 2 # 批 10 预言的 CONTROL 钉已按其自述翻转分支选择实现体与翻转后的 pin 都在,无一侧被吞。C 包升级体验 4 单的第一单落地。
升级体验这条线的真实形状(本单证伪原前提后修正,记在这里供后续两单参考):三个消费者、三份各自独立的代码 ——
消费者 文件 状态 formatZodError(spec 公开导出,defineStack抛错走它)packages/spec/src/shared/error-map.zod.ts✅ 本单已落地 zodIssuesToFields(REST wire)packages/rest/src/rest-server.ts#5014 待派 formatZodErrors(CLI 终端,os validate实际走的那条)packages/cli/src/utils/format.ts#5341 待派 后两单派发时应照抄本单已落地的分支选择策略(丢弃只报「值的种类不对」的分支 → issue 最少者胜 →
unrecognized_keys破平局 → 跨分支相同判决去重 → 绝对路径 → 深度上限),让三条路径输出口径一致,而不是各自重新发明。#4990 随本单落地解锁(两单同 touch
state-machine.test.ts,现已安全)。
Generated by Claude Code
- added 4 commits that reference this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 17, 2026 - added 3 commits that reference this issue
on Sep 8, 2026 - added 3 commits that reference this issue
on Sep 9, 2026 - added a commit that references this issue
on Oct 7, 2026
发现于 #4001 批 10(
automation/state-machine.zod.ts收紧),未在该 PR 中修复:改共享格式化器会改变全仓每一个 union 的 CLI 输出,不该搭在一个 spec 收紧 PR 上。现象
formatZodError(packages/spec/src/shared/error-map.zod.ts,从包根导出)只map顶层error.issues,从不下降进invalid_union的issue.errors[]。zod 对 union 只抛一个invalid_unionissue,其message是字面量"Invalid input",各分支的真实 issue 嵌在下一层。实测,同一个文件里的对照组(批 10 的测试里就钉着这一对):
第二例的处方并没有丢,它就在 payload 里:
REST 层(
packages/rest/src/rest-server.ts:610)把error.issues整体透传,所以 API 消费者拿得到;ZodError.message(JSON 序列化)也带着。只有压成单行的消费者会丢,而formatZodError的文档用途正是 CLI 输出(os validate/os compile)。为什么值得单开
这是本战役第三类教训(「仪器谎报覆盖率」)的一个变体,而且方向更糟:不是闸门漏报,是拒绝信息本身在到达作者前被裁掉。战役自己的规矩是「拒绝信息里的散文是行为,不是文档」—— 对 union 后面的每一个 schema,这条在 CLI 路径上今天不成立。
受影响面不止
ActionRef/GuardRef:任何 union 成员是strictObject的地方都一样,ui/view.zod.ts的 union 形状是最大的一处。随着 #4001 继续收紧,union 分支只会变多。处方(仅清点,未实施)
formatZodIssue对code === 'invalid_union'递归展开issue.errors,每个分支缩进一层;或只展开最有信息量的那个分支(unrecognized_keys 优于 invalid_type,避免把 "expected string" 的噪音也印出来)。需要一并考虑的:
formatZodError是公开导出(packages/spec/src/index.ts:128),输出变化对下游是可见行为变化 —— 大概率要 changeset。view曾经因为 union 遍历「一个未知键报两次」,见 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 批 6c 的记录)。批 10 已就地钉住现状(
packages/spec/src/automation/state-machine.test.ts的CONTROL — a non-union shape renders its full prescription through formatZodError),所以修好的那天这条钉会红,提醒把断言翻过来。