Skip to content

ViewItemSchema 同时是授权形状和 Studio 往返的 wire 成员 —— 拆成两个 schema 还是保持宽松?(挡住 #4001 批 18 最后 2 站点) #5074

Description

@xuyushun441-sys

发现于 #4001 批 18。这是 finding 16(union .extend() 陷阱)的一个更深的落点 —— 不在被 .extend() 的基类上,在 union 的成员本身

事实(端到端追踪,非推断)

ViewItemSchema(packages/spec/src/ui/view.zod.ts)有两副身份:

授权门(想要 strict):

  • defineViewItem(config) 直接 .parse()
  • objectui 的 view 创建表单把 createBuildBody 的产物按真 spec ViewItemSchema 校验(app-shell/src/views/metadata-admin/view-create-body.test.ts)

wire 门(必须留开):它是 ViewMetadataSchema成员 1 —— saveMetaItem 校验每一份持久化 view body 的那个 union。

追踪链:

  1. objectui 的 pin 控件 → dataSource.updateView(objectName, vid, { isPinned: pinned })(app-shell/src/views/ObjectView.tsx:882)
  2. updateView(data-objectstack/src/index.ts:2801)先 GET 存储项,再 PUT { ...current, ...partial, name, object }
  3. 独立 ViewItem 记录的 currentviewKind config → 命中成员 1(扁平 overlay 成员被自己的 config: z.undefined() 守卫排除)
  4. 于是 body 带着 ViewItemSchema 未声明的 isPinned 到达

今天:strip 掉(且 parsed.data 被丢弃、原始 body 原样落库),保存成功,pin 生效。
收紧后:给一个已保存视图打 pin 会 422。

一句已经过时且承重的散文(批 18 已就地修正)

view.zod.ts 的块注释写:

Auxiliary Studio round-trip keys (isPinned, sortOrder, …) ride along on every shape: all four members strip-parse (no .strict()) …

写的时候是对的,现在一半错一半承重:成员 2(容器 ViewSchema)自前几批收紧起就是 strict 的(实测 { list: …, isPinned: true } 会 422)。那不是回归 —— updateView 会先 if (current?.list) current = current.list 把容器拆到内层 list config 再合并,所以容器 body 到不了这个 union。但照着这句话推理会得出错误结论,而它正是下一个人决定「成员 1 能不能关」时会读的那句。批 18 已把它换成实测描述。

同时 metadata-type-schemas.test.tsSTILL_STRIP 收尾注释把「刻意开放的那个」说成扁平 overlay(成员 3/4,它们确实显式 .strip() 并写了理由)。成员 1 从来没被这样点名过 —— 它只是碰巧还是 strip。

需要裁定

  • A. 拆:ViewItemSchema 收紧给授权门用;ViewMetadataSchema 里换成一个 .strip() 重新开放的 wire 成员 —— 与成员 3/4 已有的做法完全一致(ListViewSchema.extend(flattenedViewOverlayFields()).strip())。代价:ViewItemSchemaz.discriminatedUnion,不能直接 .extend(),需要两个变体(每个 arm 一份 strict + 一份 strip),或把 base shape 抽出来复用。
  • B. 保持宽松,在台账/JSDoc/测试三处把它记成 wire(批 18 当前做法),授权门的严格性靠 config 内层(ListViewSchema/FormViewSchema 都已 strict)与 defineViewItem 的类型层兜。

两轴分析

长期健全性:A 更对。这个文件里已经有一条被验证过的处理这种双身份的模式 —— 成员 3/4 就是「授权 schema .extend(wire 字段).strip()」。B 让同一个 schema 名同时承担两种契约,而这正是本战役反复付代价的形状(一份散文、两种姿态)。A 也让 isPinned/sortOrder 这些辅助键有个明确的声明位置,而不是靠「没人关这个成员」隐式活着。

让 AI 写的元数据难写错:强烈偏 A。B 的失败模式很具体:defineViewItem({ name, object, viewKind, confg: {…} }) —— config 打错一个字母,今天 strip 掉,得到一个没有任何视图配置的 ViewItem,parse 成功。这就是 #4001 正文里 workflows: [...] 那条(#1535)在 view 面的重演。A 之后作者会拿到点名的拒绝;B 之后这个洞永远留着,而且台账会把它记成「已决定」,再也不会有人来看。

代价要说实话:A 是这两个选项里唯一需要真正设计的 —— discriminatedUnion 的两套变体不是一行改动,而且要确认 z.toJSONSchema() 对新结构仍能出 anyOf(/api/v1/meta/types/view 喂给 Studio 的 SchemaForm 靠它),以及 lazySchema Proxy 在新结构下不复发 ADR-0089 D3a 那次 Cannot set properties of undefined (setting 'ref')

我推荐 A,两轴同向;但它是架构选择(注册 view 门到底严不严),不是姿态翻转,所以批 18 立案不猜。若排期上 A 进不了 v17 窗口,B 是可接受的中间态 —— 批 18 已按 B 落地并三处留痕,A 落地时把留痕换掉即可。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions