发现于 #5074 实施途中(只测量,未修 —— 修在 objectui 侧,本仓不动)。
事实
objectui packages/app-shell/src/views/metadata-admin/clientValidation.ts:102 的 view loader:
view: async () => {
const { ViewItemSchema, ViewSchema } = await import('@objectstack/spec/ui');
return viewSchemaForDraft(ViewItemSchema, ViewSchema);
},
viewSchemaForDraft 按 'viewKind' in value 分派:带 viewKind 的 draft 走 ViewItemSchema。
这个 loader 同时服务创建和编辑两条路。创建路没问题 —— createBuildBody(anchors.ts:291)只发声明键(已实测:#5074 收紧后仍全部通过)。编辑路有问题:编辑器打开的是一份 STORED body,而 stored body 会带 wire 键。
链条(全部实测,非推断):
- 用户在视图切换器上给某视图打 pin →
dataSource.updateView(object, vid, { isPinned: true })(app-shell/src/views/ObjectView.tsx:882)
updateView(data-objectstack/src/index.ts:2801)GET 存储项 → PUT { ...current, ...partial, name, object }
saveMetaItem 校验后原样落库(ADR-0005 §Validation),于是 isPinned 进了 sys_metadata
- 之后在 metadata-admin 里打开这条 view 记录 →
validateMetadataDraft('view', storedBody) → ViewItemSchema
#5074 之前 ViewItemSchema 是 strip,isPinned 被静默丢掉,校验通过。#5074 把它收紧成授权门之后,这一步会报 isPinned 未识别 —— 而服务端会接受这份 body(它命中 ViewMetadataSchema 的 wire 成员 ViewItemWireSchema,isPinned/sortOrder 在那里是声明键)。客户端比服务端严,方向反了。
同类键还有 sortOrder(ObjectView.tsx:931 的重排写入)。
授权门收紧是维护者裁决(#5074,2026-08-04),两轴同向;createBuildBody 那条路本来就该按授权 schema 判。问题只在于同一个 loader 拿授权 schema 去判一份 wire body。
建议(objectui 侧)
编辑一份 stored body 时改用 wire 门。spec 现在导出了可直接用的目标:
ViewMetadataSchema —— 注册的 view 元数据 schema,和服务端 saveMetaItem 判的是同一个,覆盖三种运行时形状(容器 / ViewItem 记录 / 扁平 overlay),编辑器也确实会打开后两种;
ViewItemWireSchema —— 只要 ViewItem 记录那一种时用。
创建路继续用 ViewItemSchema(那才是授权面),即「创建按授权判,编辑按 wire 判」。这样客户端与服务端同向,而 createBuildBody 的严格性一点不丢。
⚠️ 不要用「两个都试,过一个就算过」的兜底 —— clientValidation.viewShapes.test.ts 的后两条 pin 正是为挡住这种退化写的。
影响面
- 仅 Studio 客户端预校验,服务端不受影响(PUT 仍然成功)。
- 触发条件:该视图被 pin 过 / 被拖动排序过,再进 metadata-admin 编辑器。
- 严重性由 PM 定级 —— 我只测量到「客户端会对一份服务端接受的 body 报错」,没测量它是否阻塞保存按钮。
仓库:objectui(packages/app-shell/src/views/metadata-admin/clientValidation.ts)。
发现于 #5074 实施途中(只测量,未修 —— 修在 objectui 侧,本仓不动)。
事实
objectui
packages/app-shell/src/views/metadata-admin/clientValidation.ts:102的viewloader:viewSchemaForDraft按'viewKind' in value分派:带viewKind的 draft 走ViewItemSchema。这个 loader 同时服务创建和编辑两条路。创建路没问题 ——
createBuildBody(anchors.ts:291)只发声明键(已实测:#5074 收紧后仍全部通过)。编辑路有问题:编辑器打开的是一份 STORED body,而 stored body 会带 wire 键。链条(全部实测,非推断):
dataSource.updateView(object, vid, { isPinned: true })(app-shell/src/views/ObjectView.tsx:882)updateView(data-objectstack/src/index.ts:2801)GET 存储项 → PUT{ ...current, ...partial, name, object }saveMetaItem校验后原样落库(ADR-0005 §Validation),于是isPinned进了sys_metadatavalidateMetadataDraft('view', storedBody)→ViewItemSchema#5074 之前
ViewItemSchema是 strip,isPinned被静默丢掉,校验通过。#5074 把它收紧成授权门之后,这一步会报isPinned未识别 —— 而服务端会接受这份 body(它命中ViewMetadataSchema的 wire 成员ViewItemWireSchema,isPinned/sortOrder在那里是声明键)。客户端比服务端严,方向反了。同类键还有
sortOrder(ObjectView.tsx:931的重排写入)。这不是要求回滚 #5074
授权门收紧是维护者裁决(#5074,2026-08-04),两轴同向;
createBuildBody那条路本来就该按授权 schema 判。问题只在于同一个 loader 拿授权 schema 去判一份 wire body。建议(objectui 侧)
编辑一份 stored body 时改用 wire 门。spec 现在导出了可直接用的目标:
ViewMetadataSchema—— 注册的view元数据 schema,和服务端saveMetaItem判的是同一个,覆盖三种运行时形状(容器 / ViewItem 记录 / 扁平 overlay),编辑器也确实会打开后两种;ViewItemWireSchema—— 只要 ViewItem 记录那一种时用。创建路继续用
ViewItemSchema(那才是授权面),即「创建按授权判,编辑按 wire 判」。这样客户端与服务端同向,而createBuildBody的严格性一点不丢。clientValidation.viewShapes.test.ts的后两条 pin 正是为挡住这种退化写的。影响面
仓库:objectui(
packages/app-shell/src/views/metadata-admin/clientValidation.ts)。