Repository navigation
Description
在做 #5364(saveMetaItem 的 422 展开 invalid_union,PR #5596)时,为了给"修后合法 view 仍照常保存"的承重断言挑对照输入而核出的。不在那单的文件面内(落点在 packages/spec),故独立记录。
现象
ViewMetadataSchema 接受任意对象。实测(origin/main @ e900015,getMetadataTypeSchema('view').safeParse):
{"nope":1} -> ACCEPTED as {"type":"simple"}
{} -> ACCEPTED as {"type":"simple"}
{"label":"x"} -> ACCEPTED as {"type":"simple","label":"x"}
{"name":"v","object":"o","columns":"not-an-array"} -> rejected
走真实 saveMetaItem(stub engine,记录 insert):
saveMetaItem({ type:'view', name:'garbage_view', item:{ nope: 1 } })
-> {"success":true, "state":"active", "seq":1, "message":"Saved customization overlay ..."}
persisted body = {"nope":1,"name":"garbage_view"}
即:一个 { nope: 1 } 通过了 saveMetaItem 的 spec-conformance 闸门,原样落库成一条 active 的 view overlay。
原因
ViewMetadataSchema(packages/spec/src/ui/view.zod.ts 约 L2330)是四个 arm 的 union,第 4 个是:
FormViewSchema.extend(flattenedViewOverlayFields()).strip(),.strip() 丢弃未知键,而该 arm 自己没有任何必填键(type 还带 'simple' 默认值),于是"任何对象"都能匹配它并被规约成 { type: 'simple' }。第 3 个 arm(ListViewSchema…strip())要求 columns,所以还挡得住一点;form arm 什么都不挡。
.strip() 本身是有意为之且文件里写明了理由 —— 该 arm 存在就是为了驮 Studio 的往返辅助键(isPinned / sortOrder / …),.extend() 会继承严格性,#4001 把 FormViewSchema 收紧后如果不 strip 回来,平台自己写的 body 会被 422。问题不在 strip,在这个 arm 没有任何必填字段作底:一个 union arm 既 strip 又无必填,等于给整个 union 开了一张万能通行证。
为什么值得单独判
这是 Prime Directive #10 的"declared ≠ enforced":saveMetaItem 的注释与 ADR-0005 §Validation 都声明 view 的写路径做 spec 校验,而实际上 view 是唯一一个校验可以被任意 body 绕过的常见 overlay 类型。连带影响:
- 一个 AI 生成/手写出错的 view body(整份结构写歪、键名整体拼错),不会在保存时被拒,而是静默落库成一条渲染不出任何东西的 view;
- 读路径的
_diagnostics会报valid: true(它查的是同一个 schema),于是 Studio 的合法性徽章也说它没问题; - 这正好是 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 那一整轮"未知键静默剥离"要消灭的形态,只不过发生在 union arm 选择这一层,而不是单个 object schema 那一层。
可能的方向(未裁决)
- A. 给 form arm 一个最小必填底(例如
type不给默认值、或要求sections/fields之一),让"空对象"落不到任何 arm 上 —— 但要先量清 Studio 的往返 body 里到底有什么,不能把平台自己的写变成 422; - B. 在 union 前加一道最小身份前置(view body 至少要有可辨识的 view 形状),把"什么都不是"从四个 arm 之前挡掉;
- C. 判定这是
.strip()往返能力的已知代价,接受现状并在文档里写明 view 写路径不做结构保证。
落点在 packages/spec/src/ui/view.zod.ts,改动会影响生成产物(gen:schema / authorable-surface)与 /api/v1/meta/types/view 喂给 Studio SchemaForm 的 anyOf,pin 在 view-metadata-schema.test.ts / view-authoring-wire-split.test.ts,需要一次正经的裁决而不是顺手改。相关但不同:#5316 讲的是 objectui 拿"授权 schema"去校验 STORED view 的误报,与本单方向相反(那边过严,这边过松)。
Activity
os-zhuang commentedon Aug 5, 2026
分诊:
needs-user-decision+domain:spec(预挂)。packages/spec/src/ui/view.zod.tsunion 第 4 arm(.strip()且无必填底)→domain:spec;改动波及gen:schema/ authorable-surface 生成物与/api/v1/meta/types/view喂给 Studio SchemaForm 的anyOf。packages/spec恒归 spec 座位,拍板后由 spec 车道派发。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
维护者裁决(2026-08-06,经 engine-core 车道 PM 转达,会话
session_01V7WetGmnfoXNn8cLieKKmx):取 方向 B —— 在 union 之前加一道最小身份前置(view body 至少要有可辨识的 view 形状),把「什么都不是」在四个 arm 之前挡掉;不动各 arm 的.strip()往返能力。方向 A(form arm 加必填底)作为 B 之后的可选二次收紧,须先实测 Studio 往返 body 再议;方向 C 否决。落点
packages/spec/src/ui/view.zod.ts,归 spec 座位;请分诊照此入队(决定已做,⛔ 不挂 needs-user-decision)。实现注意:改动影响gen:schema/authorable-surface生成物与/api/v1/meta/types/view的anyOf,pin 在view-metadata-schema.test.ts/view-authoring-wire-split.test.ts;_diagnostics(#5598)与本单同 schema,B 落地后其valid:true误报自然消失,验收时一并断言。Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
状态翻转:
needs-user-decision→pm:queue(执行上一条维护者裁决的入队指示)。派发口径 = 方向 B:union 前加最小身份前置,不动各 arm 的.strip();方向 A 作为可选二次收紧须先实测 Studio 往返 body;验收含_diagnostics(#5598)误报消失断言与 pin 更新。归 spec 座位。会话:session_01N3uGFF8teXbpgtbEJ1aYXuGenerated by Claude Code
test(spec): the third ui/ file group's test titles state each cited d…
test(spec): the last ui/ file group's test titles state each cited de…