Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): 元数据保存的 422 保留 union 分支处方,Studio 重新拿得到字段名 (#5364) - #5596
Conversation
…#5364) The spec-conformance 422's own comment promises "structured Zod issues so the Studio form can highlight the offending field". A top-level `z.union` broke that completely: zod folds every branch of a failed union into ONE issue whose path is '' and whose message is the literal "Invalid input", and `parsed.error.issues.map(...)` mapped exactly that. Since `ViewMetadataSchema` IS a top-level union, EVERY failed view save arrived at Studio as one rootless line with no field name in it at all. `zodIssuesToMetadataIssues` expands the branches that explain the rejection, resolving branch-relative paths against the union's own. The union's own entry is kept, so the change is strictly additive. Branch selection is byte-for-byte the ranking `formatZodError` (#4971, spec) and `zodIssuesToFields` (#5014, rest) already use: drop kind-mismatch-only branches, fewest issues wins, `unrecognized_keys` breaks the tie, declaration order breaks the rest, ties all emitted (cap 3), nested unions recursed (depth 3). Three copies, one verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…emeta-union-branches
os-zhuang
commented
Aug 5, 2026
CI 红说明(engine-core 车道 PM):ESLint job 的失败是 Generated by Claude Code |
…emeta-union-branches
Uh oh!
There was an error while loading. Please reload this page.
…ack-ai#5598) (objectstack-ai#5765) computeMetadataDiagnostics 把 zod 的 error.issues 直接 .map() 成 _diagnostics 条目。zod 会把一个失败 z.union 的全部分支折叠成一条顶层 issue —— path 为空、 message 是字面量 "Invalid input" —— 而 ViewMetadataSchema 顶层本身就是 union, 所以库里每一个有缺陷的 view 读出来都退化成这一条没有字段名的记录,模块头承诺的 "内联字段错误"无处可标。 后果是同一份文档在两条路径上判决不一致:objectstack-ai#5364(PR objectstack-ai#5596)修好写路径之后,保存 一个有缺陷的 view 能看到出错的键名,打开同一份已存库的文档却仍然只有一条 Invalid input。 改法是复用而非再抄一份策略:读路径改调同包 objectstack-ai#5596 落地的 zodIssuesToMetadataIssues,分支选取口径由该函数单点定义,读写两路径按构造一致。 展开是纯增量 —— union 自己那条仍在 errors[0]。stripDiagnostics 一段未动。 Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx Co-authored-by: Claude <noreply@anthropic.com>
Fixes#5364
> 排版说明:下文一律用
⟨root⟩指代源码里那个 ASCII 尖括号字面量(小于号 + root + 大于号,见i.path || ...那行的 fallback)。GitHub 的正文消毒器会把小于号紧跟字母整段当 HTML 标签删掉,本 PR 初版正是这么被吃掉了一次,故改用这对括号写。✅ 停放已解除(PM 维护,2026-08-05)
曾因 base 分支的 #5604 红(
check-engine-double-contract挂action-execution-calldata-not-found.test.ts)停放;#5615(2614aef)落地后本分支已同步 main(head2cf9cc0),CI 23 项全绿,按 ACCEPT 流程转 ready 入队。前提复核(在 origin/main
e900015上实测)先按「issue 是线索不是规格」核了一遍。issue 引用的
protocol.ts:7128已漂移,INVALID_METADATA现在有两处:L7010 是「分层读信封当写体」的防御性拒绝(与本单无关),L7141 才是 saveMetaItem 的 spec-conformance 422。缺陷本身则完全成立,走真实saveMetaItem路径复现:被
.map()丢掉的 4 个分支里躺着的是:branch[1] 的unrecognized_keys(点名type、columns,带 #4001 策展处方)、branch[2]/[3] 的invalid_value(带槽位路径与合法枚举)。改了什么
packages/metadata-protocol/src/protocol.ts新增zodIssuesToMetadataIssues,422 的issues由它产出。union 自己那条保留不动——展开是严格叠加的,今天读issues[0]的消费者不会少读到任何东西——后面跟上真正解释这次拒绝的分支,路径按绝对路径拼好(分支 issue 的path是相对于 union 的,#5014 为此付过学费)。422 的message摘要行随之变得可读。分支选择策略与 main 上两份参考实现逐条一致:丢弃只报根部 kind 不匹配的分支;报得最少的分支胜出;
unrecognized_keys破平局;声明顺序决定其余;并列全出(上限 3);嵌套 union 递归(上限 3 层)。这是同一机制的第三份拷贝,形态不同而判决相同:spec 的formatZodError(#4971)只导出字符串渲染器,rest 的zodIssuesToFields(#5014)产出 ADR-0114 的{ field, code }目录条目,本处信封是{ path, message, code }且code透传 zod 原码(按派发约束,不顺手对齐 ADR-0114 目录 —— 那需要单独裁决)。修后实测
issue 那个 view 输入,
issues从 1 条变成 2 条,第二条code是unrecognized_keys,message 点名`type`、`columns`并带 #4001 的完整处方;摘要行从... failed spec validation: ⟨root⟩: Invalid input变成... failed spec validation: ⟨root⟩: Invalid input; ⟨root⟩: Unrecognized key(s) on this view container: ...。派发约束写的是「修后
issues必须含带真实键名的unrecognized_keys条目与带type路径的invalid_value条目」。用 issue 那个输入,后者不会出现,而且这不是实现缺陷:unrecognized_keys→ rank[1, 0][2, 1]「报得最少的分支胜出」是排序第 2 位,
unrecognized_keys破平局是第 3 位,所以只有 branch[1] 入选。要同时输出两者就必须改排序策略,而那会让四个消费者判决不一致——正是本单存在的理由。故按「语义必须与两份参考一致」执行,并把「带type路径、可高亮」的承重断言放到确实产出它的输入上(容器体{ list: { type: 'nope', ... } }→path为list.type、code为invalid_value、message 含合法枚举)。测试
新增
packages/metadata-protocol/src/protocol.save-union-issues.test.ts(14 例):issues[0];摘要行可读list.type绝对路径 +invalid_value+ 合法枚举rows.size === 0errors: []变体不加噪;最少胜出 +unrecognized_keys破平局;并列全出且上限 3;逐层拼绝对路径;第 4 层 union 不再展开;同键同话去重一次;去重不跨顶层 issue;非数组入参返回空信封反向验证(方向事先预判,结果与预判一致)
把调用点还原成旧的
.map(),预判「只有两条 save 路径的展开断言转红,其余 12 条(zod 事实钉、合法保存对照、10 条纯策略单测)保持绿」。实跑:影响面(按规则消费半径,不按被改包)
err.issues的下游 fixture 全扫了一遍:packages/objectql的protocol-meta.test.ts/metadata-validation-sweep.test.ts只断言Array.isArray(issues)与 code/status,packages/rest的两个信封测试自建issues数组——都不钉条数或内容,故不受影响。最终同步圈(head2cf9cc0,base 含2614aef)三包实跑:typecheck:
@objectstack/metadata-protocol无typecheck脚本(在check-type-check-coverage.mjs的 DEBT 台账里)。改用直跑 tsc 对比基线,63 → 63,未增一条;pnpm check:type-check-coverage与node scripts/check-nul-bytes.mjs均 OK;check:engine-double-contractOK(26 pinned);#5600 新落的 query-options-erasure ratchet 绿且本 PR 未进其基线。范围纪律
packages/spec(含strictness-batch14.test.ts:191/dashboard-compareto.test.ts:201的 docblock 校准 —— 那两句「zodIssuesToFields只映射顶层 issue」在 PR fix(rest): zodIssuesToFields 展开 invalid_union,联合分支里的处方到达调用方 (#5014) #5362 合入后对 REST 面已失真,本 PR 又让它对 saveMetaItem 面失真;按派发约束不动,债记在 元数据保存的 422 也丢掉 union 分支处方:一个 view 保存失败只回一条path:"" message:"Invalid input",Studio 无字段可高亮 #5364 时间线)packages/rest/packages/cli(union 分支里的 unknown-key 处方永远到不了作者:zodIssuesToFields只映射顶层 issue,失败的 union 只剩Invalid input#5014 已修、os validate/os build用的是 CLI 自己的 formatZodErrors,它同样把 union 分支的处方裁掉 —— #4971 修的不是这条路径 #5341 待派)content/docs/releases/;.changeset/savemeta-422-union-branch-issues.md已附packages/spec/authorable-surface.base.json(gen:schema重锚baseRev)已还原,不在本 PR 的 diff 里顺带发现(已另立 issue,不在本 PR 修)
_diagnostics是 union 折叠的第五个消费者:Studio 打开一个有缺陷的 view,内联字段错误同样只有一条path:"" Invalid input#5598 —— 同一折叠机制的第五个消费者:metadata-diagnostics.ts:77的computeMetadataDiagnostics(读路径_diagnostics信封,同形同病)。本 PR 修好后,保存一个有缺陷的 view 能看到字段名,打开一个存量有缺陷的 view 仍然只有一条无字段的Invalid input。view的 spec 校验闸门形同虚设:saveMetaItem({ item: { nope: 1 } })返回 success 并把{"nope":1}存成一个 active view #5599 ——view的 spec 校验闸门形同虚设:saveMetaItem({ item: { nope: 1 } })返回 success 并把该 body 存成一条 active view。ViewMetadataSchema第 4 个 arm(FormViewSchema.extend(...).strip())既 strip 又无必填键,等于给整个 union 开了万能通行证。落点在packages/spec,需要单独裁决。🤖 Generated with Claude Code
https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx