Repository navigation
Description
#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),或者在只有一个分支「明显是作者意图的那支」时优先提升该分支。后者更难判定但输出更干净 —— 对象输入配对象分支,是一个可用的启发。
fields[] 的条数会变),按 ADR-0114 需要和 mapDataError 保持同形。
批 14 已在 dashboard.zod.ts 的 compareTo 处记下这条实测限制,并在测试里分开 pin 了两件事:作者今天真正看到的裸 message,和等着被传递的处方文案 —— 免得一个绿测试冒充一条没人打印的消息。
发现于 #4001 批 14 的范围外体检,未指派。
Activity
claude commentedon Aug 4, 2026
分诊纠偏:修复落点实测为
packages/rest/src/rest-server.ts(zodIssuesToFields),按 Domain lanes 锚定规则应为domain:cli(rest 暂归类),原domain:engine有误,已更正。归 cli 车道 PM 视野。附注:issue 建议的「普查 spec 里 strictObject 落在 union 分支内的数量」属 spec 车道联动,接手 PM 可拆单。Generated by Claude Code
feat(spec)!: tighten unknown keys on nine ui/ shapes, reclassify two …
feat(spec)!: view.zod.ts 子块收紧 —— 20 站点收 15 留 5(objectstack-ai#4001 批 …
fix(spec): 重开 ViewFilterRuleSchema —— 控制台盖的 UI 行 id 让保存筛选条件 422(objec…
os-zhuang commentedon Aug 5, 2026
认领:PM 循环第 2 轮(v17 C 组,升级体验三消费者之二)
会话:
session_01FTszibd6C8sUCCZnM4VcrL分支:
claude/issue-5014-rest-union-fieldsWorktree:
objectstack-issue-5014域:
domain:cli(沿本单 08-04 04:56Z 的分诊纠偏,rest 暂归类)文件面:
packages/rest/src/rest-server.ts(zodIssuesToFields)+ 其测试 +.changeset/*。⛔ 不碰packages/spec/**(#4990 在飞于shared/suggestions.zod.ts)、⛔ 不碰packages/objectql/**(#5351 的 engine 面待维护者裁决)。派发时机说明:#4971 已于
546ab3c49合入 main,其分支选择策略现可在 main 上直接读到并照抄,不必重新发明 —— 这正是本单等到现在才派的原因。同日 churn 核查:rest-server.ts在 main 上最近一次改动是b8503183c(与本单无关)。Generated by Claude Code
1 remaining item
docs(api): zodIssuesToFields 的条目数不再等于 zod issue 数 (#5014)
os-zhuang commentedon Aug 5, 2026
复核补记(我的 ACCEPT 之后 PR 又推了一个提交,
8567cdb88):只动
content/docs/api/error-handling-server.mdx(+6 行,零releases/改动),内容是把「fields.length等于 zod issue 条数」改成「读作字段错误条数」。这一步值得单独肯定,因为它对一条 advisory 做了正确的区分。
docs-drift-check对本 PR 列了 11 页,绝大多数是包级泛化关联(改任何@objectstack/rest文件都会列出来),按惯例是噪音;但其中error-handling-server.mdx正是对外推荐zodIssuesToFields的那一页,而本 PR 恰好改变了它教给读者的那个可观测性质。dev 没有把整条 advisory 当噪音略过,也没有为了「响应 advisory」去动那 10 页无关的,而是准确挑出了唯一真受影响的一页。契约文档与契约在同一个 PR 里一起变,这是 declared = enforced 的最基本形态 —— 否则本 PR 落地当天,那一页就成了下一个 #5269(教着一个已经不成立的事实)。
判决不变:ACCEPT。
Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
已落地 —— PR #5362 经合并队列合入 main(
2d2530300)会话
session_01FTszibd6C8sUCCZnM4VcrL。落地判据复核:三处都在,契约与教它的文档一起落地。
升级体验这条线的现状
formatZodError(defineStack)546ab3c49zodIssuesToFields2d2530300formatZodErrors(os validate实际走的)saveMetaItem的 422(Studio 无字段可高亮)error-map.zod.ts的字符串渲染版、本 PR 的结构化条目版)。四个消费者判决不一致,等于同一个错误在 CLI、API、Studio 三处给出三套说法。并注意本单踩过的陷阱:分支 issue 的path相对于 union,不拼绝对路径就会读错槽位。本会话按维护者暂停指示,#5341 与 #5364 均不派,留给下一任。
Generated by Claude Code
feat(spec)!: object 内层块拒绝未知键 —— 13 站点收紧,2 个站点依据实测保留 (objectstack-ai#4001
fix(rest): zodIssuesToFields 展开 invalid_union,联合分支里的处方到达调用方 (objectst…
fix(cli): formatZodErrors expands invalid_union, the branch prescript…
fix(metadata-protocol): expand union branches in the saveMetaItem 422 (…
docs(audits): the strictness ledger stops asserting a union flattenin…
docs(audits): correct the strictness ledger's fourth union-flattening…
docs(audits): the strictness ledger stops asserting a union flattenin…
docs(audits): correct the strictness ledger's fourth union-flattening…
feat(spec)!: a metric-family dashboard widget declares exactly ONE me…
test(spec): the fourth data/ file group's test titles state each cite…
test(spec): the last ui/ file group's test titles state each cited de…