Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): expand view edit-gate union failures into per-field diagnostics - #3624
Merged
Merged
Conversation
… diagnostics The edit gate is `ViewMetadataSchema` = `z.preprocess(strip, z.union([...]))` (objectstack#5316 / #3607). Zod reports a union failure as ONE root issue — `path: []`, `message: 'Invalid input'` — and buries each member's real diagnostics in `issue.errors`, one group per member. Mapping that root issue literally collapsed the entire edit path onto a single un-addressable message: `SchemaForm` highlights by `path` and Monaco locates by `path`, so an empty path points at nothing, and the guided rejection messages the spec authored (#4001) never reached the editor. Failures are now expanded to the union member the draft's OWN discriminant selects — the same `viewKind` test the create gate dispatches on, now factored into a single `isViewItemDraft` so the two cannot drift. Only the selected member's issues are rendered; showing all four would put "this is not a view container" in front of someone editing a ViewItem. The rejected heuristic alternative (fewest issues / deepest path) is pinned as wrong by a test. Presentation only — the verdict cannot move. The expansion runs strictly inside the issue-to-form-issue mapping, downstream of `ok` and of every issue filter, so the set of raw issues driving `ok` is untouched. A parity block pins `ok` across three runtime shapes x both modes. Member selection is positional, which couples to a spec-internal detail. That is deliberate: reading the groups the failing gate itself produced is the only way the diagnostics cannot describe a schema other than the one that judged. Re-parsing with the exported member schemas was measured and rejected — the container member is not the exported `ViewSchema` object, and a re-parse would have to re-apply `stripViewConsoleDecorations` itself, reconstructing the spec's pipeline in a second place that can drift. Canary tests assert the selected member's exact path + message, so reordering, adding or removing a union member fails loudly instead of silently mis-selecting. Fixes#3606 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 7, 2026 16:22
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3606
问题
#3607(objectstack#5316)把
view编辑路切到 wire 门ViewMetadataSchema—— 这是对的,它正是view元数据类型注册、服务端saveMetaItem所判的那个 schema。但它是z.preprocess(strip, z.union([...])),而 Zod 对 union 失败只产出一条根级 issue(code: 'invalid_union',path: [],message: 'Invalid input'),四个成员各自的真实诊断埋在issue.errors里(每个成员一组,按成员序)。validateMetadataDraft的映射是逐字照搬那条根 issue,于是编辑路的逐字段诊断全部塌陷。SchemaForm按path做内联高亮、Monaco 按path定位 —— 空 path 指向不了任何东西;spec 为这些拒绝专门撰写的引导消息(#4001 那批)在编辑路上完全看不到。改动
失败之后,按 draft 自身的判别式选定 union 成员,展开该成员的 issues。判别式复用创建路已有的
viewKind判定,这次抽成单一的isViewItemDraft,两条路不可能再各自漂移。只呈现选中成员的 issues —— 四个成员全摆出来会把「这不是 view container」摆到正在编辑 ViewItem 的人脸上。issue 里提到的启发式选支(issue 数最少 / path 最深)已被一条测试钉死为错误做法。
实测:修前 / 修后
编辑路(
mode: 'edit'),@objectstack/spec17.0.0-rc.5:config.type: 'not_a_real_layout'Invalid inputconfig.typeInvalid option: expected one of "grid"|"kanban"|"gallery"|…nameInvalid inputnameInvalid input: expected string, received undefinedconfig.filter[0].operator非法Invalid inputconfig.filter.0.operatorInvalid option: expected one of "equals"|"not_equals"|…notAContainerKeyInvalid inputUnrecognized key(s) on this view container: `notAContainerKey`. Until #4001 closed these shapes an unknown key was dropped silently — …columnsInvalid input• `columns` belongs to a single VIEW, not to the container. Wrap it: `defineView({ list: { type, data, columns, … } })`, or name it — …对 issue 正文的一处事实订正:正文把容器拒绝的消息引成
Unrecognized key(s) on this view container: … Wrap it: defineView({...})。实测这两截不是同一条消息 ——Wrap it: defineView(...)是 spec 只挂在属于单个 view 的那批键(type/columns/data/viewKind/filters/sort)上的逐键提示;notAContainerKey这种普通未知键拿到的是不带该提示的通用消息。表格最后两行分别钉住这两种,避免以后有人照 issue 正文去断言而扑空。判定奇偶性:只有呈现变了
展开逻辑跑在 issue →
SchemaFormIssue的映射内部,位置在ok判定之后、且在既有的serverRequired/ flow 前向兼容两个过滤器之后 —— 驱动ok的那组 raw issues 一个都没动。所以判定不是「测出来没变」,而是结构上不可能变。这一点仍然写成了 pin(
view verdict parity一节):三种运行时形状 × 合法/非法 × 两种 mode,逐一断言ok。它修前修后都是绿的 —— 这正是它的意义,它是防回归的那一半,不是「修前红修后绿」的那一半。若有人日后把这里改成 try-both 兜底改判,这组期望会翻成true而爆掉。clientValidation.viewShapes.test.ts的现有 pin 一行未改,原样通过。创建路(ViewItemSchema/ViewSchema,都不是根 union)行为零变化,并按精确 path+message 钉住,而不是只靠论证。为什么按成员序索引,而不是拿导出的成员 schema 重解一遍
两条都测量过:
ViewMetadataSchemaunion 的options[0]就是导出的ViewItemWireSchema(同一个对象);但options[1]不是导出的ViewSchema—— 今天二者 shape 相同、行为相同,但那是手工维持的相似,正是 #5316 在本文件注释里明确拒绝依赖的东西。stripViewConsoleDecorations,把 spec 的 preprocess 组合在第二个地方复刻出来 —— 又一处会漂移的复制品(config.filter[].id那条 false positive 就是这么来的)。读「判定时那一次运行自己产出的」错误组,是唯一能保证诊断不会描述另一个 schema的做法。代价是耦合成员顺序(spec 内部细节),这个代价用 CANARY 测试兜住:它们断言选中成员的精确 path + message,spec 一旦重排/增删 union 成员,测试直接红,而不是静默误选。
已知残留(未修,不在本 issue 范围)
只展开根上的 union。成员 issue 的
path相对于 union 节点,只有在根上它才等于 draft 绝对路径。更深处的嵌套 union(实测:config.columns是string[] | ColumnDef[],无判别式)仍保留 Zod 自己的Invalid input—— 但它带着真实 pathconfig.columns,是可定位的,这与根上那条空 path 有本质区别。已在a rejected draft always renders at least one issue里覆盖到,不会静默丢诊断。测试
新增
packages/app-shell/src/views/metadata-admin/clientValidation.viewDiagnostics.test.ts(18 条:5 CANARY + 2 噪音控制 + 2 兜底 + 2 创建路 + 7 判定奇偶)。反向验证(先写预测再跑):还原实现后 7 红(5 CANARY + 2 噪音控制),打回实现后 0 红;11 条判定奇偶/创建路/兜底测试两个方向都绿。
有一处预测未命中,如实记录:两条噪音控制测试最初修前也是绿的 —— 它们只有
not.toContain(...),而塌陷后的Invalid input恰好也不含任何被禁子串,即「因为什么都没产出所以绿」。已各补一条正向锚点断言,补后两条都变成修前红、修后绿。Generated by Claude Code