Skip to content

fix(spec): formatZodError 展开 union 分支的拒绝信息 (#4971) - #5342

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-4971-union-error-prose
Aug 5, 2026
Merged

fix(spec): formatZodError 展开 union 分支的拒绝信息 (#4971)#5342
os-zhuang merged 5 commits into
mainfrom
claude/issue-4971-union-error-prose

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #4971

前提复核(先证,再改)

issue 的核心断言在 origin/main(c7406b0)上成立,并且是实测的:

=== ActionRefSchema.safeParse({ type: 'log', args: { a: 1 } }) ===
issues[0] = { code: 'invalid_union', path: [], message: 'Invalid input',
              errors: [ [ {code:'invalid_type', message:'... expected string ...'} ],
                        [ {code:'unrecognized_keys', keys:['args'],
                           message:'Unrecognized key(s) on this action reference: `args`. Until #4001 …'} ] ] }

formatZodError →
Validation failed (1 issue):

  ✗ (root): Invalid input

formatZodErrormap 顶层 error.issues,从不下降进 issue.errors[](改前 error-map.zod.ts:186)。处方一直在 payload 里,丢的只是压成单行的消费者。

一处需要更正的前提(不影响本单结论):issue 说「formatZodError 的文档用途正是 CLI 输出(os validate / os compile)」—— JSDoc 确实这么写,但那两个命令实际调的是 CLI 自己的 formatZodErrors(packages/cli/src/utils/format.ts:167),它有同样的缺陷。已另单记录为 #5341(不在本单文件面内)。本单的修复并非空转:defineStack 的严格模式抛错走的就是 formatZodError(packages/spec/src/stack.zod.ts:1250),而任何加载 stack 配置的命令都会执行它 —— 作者在 union 里写错一个键,今天起就能读到处方。

改了什么

formatZodIssuecode === 'invalid_union' 递归展开 issue.errors,缩进一层、路径拼成绝对路径:

Validation failed (2 issues):

  ✗ id: Identifier must be at least 2 characters
  ✗ states.s.on.GO: Invalid input
    ✗ states.s.on.GO.actions.0: Invalid input
      ✗ states.s.on.GO.actions.0: Unrecognized key(s) on this action reference: `args`. Until #4001 …

全印一遍是本单唯一必须避免的失败模式(#4001 批 6c:一个未知键被 N 个分支各报一次,view.zod.tssubmitBehavior 因此改用 discriminatedUnion),所以分支是的,不是倒的:

  1. 只在自己根上抱怨「值的种类不对」的分支(z.union([z.string(), SomeObject])z.string() 那支喊 expected string, received object)不携带处方,丢弃。当所有分支都是这一类时(z.union([z.string(), z.number()]) 收到一个对象)完全不展开,输出与改前逐字节相同 —— 原始类型 union 是全仓最常见的 union,给它加 N 行噪音换零处方是负收益。
  2. 其余分支里 issue 最少的胜出:作者真正在写的那个成员只抱怨那一个多余的键,其它成员还会抱怨判别式对不上和自己缺的必填项。这就是「一个未知键只报一次」的机制本身,不是额外的去重补丁。
  3. unrecognized_keys 破平局(策展散文在那儿),声明顺序兜底 —— 输出确定。
  4. 真正打平的分支全部渲染(上限 3,超出的印一行「… and N more branches rejected this value」):两个形状同样能解释失败时,按声明顺序偏袒第一个是在撒谎。跨分支逐字相同的判决只印一次。
  5. 嵌套 union 递归展开,深度上限 3 层(输出有界)。

表头的 issue 数仍是 error.issues.length —— union 无论用几行解释都是一条 issue,CLI 与结构化消费者(REST 错误体、ZodError.message)对「错了几处」保持同一口径。

反向验证(方向:红,如期)

先写断言、临时把 error-map.zod.ts 还原成 origin/main 再跑:

FAIL src/automation/state-machine.test.ts > [#4001] ActionRef / GuardRef — strict inside a union > CONTROL — union and non-union shapes both render their prescription through formatZodError
FAIL src/shared/error-map.test.ts > … > renders the failing branch prose under the union line
FAIL src/shared/error-map.test.ts > … > resolves nested paths against the union, not relative to it
FAIL src/shared/error-map.test.ts > … > expands a union nested inside a union
FAIL src/shared/error-map.test.ts > … > reports one unknown key ONCE, not once per branch
FAIL src/shared/error-map.test.ts > … > de-duplicates a verdict every tied branch reaches
FAIL src/shared/error-map.test.ts > … > stops expanding after three levels of nesting
 Tests  7 failed | 38 passed (45)

上表 7 条 = 6 条新钉 + 1 条被翻转的 CONTROL 钉。第 7 条新钉(caps a wide tie and says how many branches it did not print,后补,钉的是渲染上限那条此前无人到达的分支)单独按同样方式量过:

FAIL … > caps a wide tie and says how many branches it did not print
AssertionError: expected 'Validation failed (1 issue):\n\n  ✗ (…' to contain '… and 2 more branches rejected this v…'

所以新增的 10 条钉里 7 条改前必红,全部实测;另外 3 条(drops the kind-mismatch branch…leaves an all-kind-mismatch union exactly as it wascounts the union as the one issue zod raised)两边都绿是设计如此 —— 它们钉的是「不许变」的不变量(原始类型 union 输出不动、表头计数不动),不是本次的行为变更,如实记在这里而不是凑成「全红转全绿」。

批 10 预言的那条钉(state-machine.test.ts 的 CONTROL)按其自述翻转:改前断言 not.toContain('this action reference'),现在断言它在,并额外钉住 expected string 这类噪音不被打印。

消费半径普查

formatZodError / formatZodIssue / safeParsePretty 在本仓的唯一非测试调用点是 packages/spec/src/stack.zod.ts:1250(defineStack);objectui / cloud 两个兄弟仓零引用(全文扫描)。packages/clipackages/core 各有同名的本地格式化器,不经过这里(见 #5341)。packages/spec 全量测试通过,说明没有第二条钉在这次输出变化上变红。

相关但在本 PR 里修

文件面

  • packages/spec/src/shared/error-map.zod.ts —— 分支选择 + 递归渲染
  • packages/spec/src/shared/error-map.test.ts —— 10 条新钉
  • packages/spec/src/automation/state-machine.test.ts —— CONTROL 钉翻转
  • packages/spec/src/automation/state-machine.zod.ts —— 仅注释(声明的文件面外一处):该 JSDoc 原文陈述「formatZodError 是丢弃者之一,filed 而非在此修」,本 PR 之后这句是假的,留着会误导下一个读者
  • .changeset/format-zod-error-union-branches.md —— @objectstack/spec: minor(公开导出的输出行为变化)

验证

pnpm --filter @objectstack/spec test          → Test Files 312 passed (312) · Tests 8032 passed (8032)
pnpm --filter @objectstack/spec typecheck     → tsc --noEmit,无输出
pnpm --filter @objectstack/spec check:api-surface   → public API surface + factory signatures unchanged ✓
pnpm --filter @objectstack/spec check:exported-any  → 1845 types + 1592 schemas,无 any
npx eslint --no-inline-config (4 个改动文件)  → 无输出
node scripts/check-nul-bytes.mjs              → OK(5379 个文件,无裸 NUL);另按控制字符自查全部改动文件,零命中

🤖 Generated with Claude Code

https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL

zod 对失败的 union 只抛一个 `invalid_union` issue,其 message 是字面量
"Invalid input",各分支的真实 issue 嵌在 `issue.errors[]` 里。
`formatZodError` 只 map 顶层 `error.issues`,从不下降,于是 union 后面
每一个 strictObject 的策展散文在 CLI 路径(`os validate` / `os compile`)
上都被裁成 `✗ (root): Invalid input`。REST 错误体和 `ZodError.message`
一直带着这份 payload —— 丢的只是压成单行的消费者。

现在按信息量挑分支展开:只报"值的种类不对"的分支(union 里 z.string()
成员对着对象喊 expected string)不携带处方,直接丢弃;全部分支都是这一类
时(z.union([z.string(), z.number()]))完全不展开,输出与改前逐字节相同。
其余分支里 issue 最少的胜出 —— 作者真正想写的那个成员只抱怨那一个多余的
键,其它成员还会抱怨判别式和自己的必填项 —— 这正是"一个未知键不被报 N 次"
(#4001 批 6c 的回归)的机制;`unrecognized_keys` 破平局,声明顺序兜底。
真正打平的分支全部渲染(上限 3),跨分支相同的判决只印一次。嵌套 union
递归展开,路径为绝对路径,深度上限 3 层。

表头的 issue 数仍是 `error.issues.length`,CLI 与结构化消费者对"错了几处"
保持同一口径。

- packages/spec/src/shared/error-map.zod.ts: 分支选择 + 递归渲染
- packages/spec/src/shared/error-map.test.ts: 9 条新钉(含 6 条改前必红)
- packages/spec/src/automation/state-machine.test.ts: 批 10 的 CONTROL 钉
  按其自述翻转
- packages/spec/src/automation/state-machine.zod.ts: 仅注释,原文陈述的
  "formatZodError 是丢弃者之一"已不再成立

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 12:07am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 4 commits August 4, 2026 23:51
selectUnionBranches 的 UNION_BRANCH_RENDER_LIMIT 与 omitted 提示行此前
没有测试到达:需要 3 个以上分支真正打平才会触发。五个互不相交的
strictObject 分支对同一个多余键正好构成这种平局。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
os validate / os build 走的是 CLI 自己的 formatZodErrors,不是这里的
formatZodError;后者的实际到达面是 defineStack 的抛错。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
os validate / os build 的 schema 报错走 CLI 自己的格式化器;本包这份的
到达面是 defineStack 的抛错。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTszibd6C8sUCCZnM4VcrL
@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 00:23
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 546ab3c Aug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4971-union-error-prose branch August 5, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

formatZodError 把 union 分支的拒绝信息压成 "Invalid input" —— #4001 策展的散文在 CLI 路径上到不了作者

2 participants