Part of objectstack-ai/objectui#3797
按跨分片移交协议从 objectui 分片转来:packages/spec 是共享契约面,objectui 侧不自行修改(objectui AGENTS.md #0 / #0.1)。objectui 那一侧只落门 (全仓 registry inputs × ComponentPropsMap parity 测试 + 显式豁免名单),这 5 个 prop 在门里挂本单号等上游。
机制:两个平台权威今天对同一个键公开互相矛盾
objectui 的 registry inputs 是对外发布的授权面 ,不是文档:packages/sdui-parser/scripts/gen-manifest.ts 把它序列化进 sdui.manifest.json(save-gate + parser 白名单)和 sdui-intrinsics.d.ts(JSX 授权类型面);packages/components/src/renderers/layout/page.tsx:462 另外用 getKnownTypes() 在运行时现搭一份 JSX 页面校验 manifest。
而自 #5068 起,packages/lint/src/validate-component-props.ts 按 type dispatch,用 lintUnknownKeysAgainstSchema 判 properties 的未声明键,os validate / os build / os lint 都跑。
于是下面这 5 个键今天的处境是:
objectui 的 manifest / 生成的 .d.ts 告诉作者(尤其是 AI 作者)这个键合法 ;
validateComponentProps 对同一个键报 warning:未声明;
渲染器确实读它 ,行为对用户可达。
三者不可能同时对。这正是 #5435 「平台权威不得指向自己闸门会拒绝的键」的场景,只是这一次错的一半在 spec 的声明面。
顺带修正一处上游注释的事实 :packages/lint/src/authoring-rules.ts:564-566 现在写着
#5775 has since settled its half: displayField is retired in favour of the labelField the renderer actually reads, and the rest of the keys the renderers honour are declared. #5728 and two page rewrites are what remain.
「the rest of the keys the renderers honour are declared」这句不成立 —— 下面 5 个键渲染器 honour、spec 未声明。因为 validateComponentProps 升 error 的前置条件写的是「warning-period inventory is empty」,这 5 条就是那份 inventory 里还没被记进去的部分,直接卡在升级路上。
清单(objectui 侧 file:line 证据,均以 objectui origin/main 为准)
渲染器读点全部在 packages/components/src/renderers/layout/containers.tsx。这些读的都是 schema.* / schema.properties.*,也就是作者写的页面元数据 ;该文件真正由宿主注入的东西走 RecordContext(headerSystemActions / isFavorite / onToggleFavorite),并且故意没有 出现在 inputs 里 —— 所以下面 5 条不是「renderer-only 内部 prop」,是作者配置项。
1. page:header.recordChrome(boolean,默认 true)
读点:containers.tsx:979 — schema?.recordChrome === false || schema?.properties?.recordChrome === false;消费点 containers.tsx:1453(注释原文:Author hasn't opted out via recordChrome: false)决定走「记录 chip 版页头」还是「裸 h1 版页头」。
仓内已有作者在写:apps/console/src/preview-samples.ts:68 — { type: 'page:header', properties: { title: 'Welcome to the CRM', recordChrome: false } },而这条恰好是 validateComponentProps 今天会报 warning 的形状 ;packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:413-414 也在每个合成的记录页页头上产出它。
语义:非记录页(dashboard / 落地页)用它退回历史的裸 h1 布局。
建议声明:recordChrome: z.boolean().default(true),描述写「false 时不渲染记录 chip,退回裸 h1 页头(非记录页用)」。
2. page:header.showStar(boolean,默认 true)
读点:containers.tsx:980;消费点 containers.tsx:1531 → RecordTitleChip showStar(packages/components/src/custom/RecordTitleChip.tsx)。
语义:关掉标题旁的关注(收藏)星。
3. page:header.showCopyId(boolean,默认 true)
读点:containers.tsx:981;消费点 containers.tsx:1532 → RecordTitleChip showCopyId(RecordTitleChip.tsx:47/64/114)。
语义:关掉「复制记录 ID」按钮。
关于这三条的一个已被推翻的猜测 ,记在这里省得再走一遍:objectui#3797 正文提示「page:header 的 inputs 在 objectui#3226 / PR #3265 刚被收窄过,所以这三个可能是有意保留的 renderer-only prop」。核验为否 —— PR #3265 (objectui d2363e710)动的是 packages/layout 里的遗留 kebab 别名 page-header (registerLayout() 的 description input,对应本仓 #4827 ),packages/components 里 canonical 的 page:header 那次没被碰过 。没有任何「有意保留」的记录,这三条就是普通的未声明作者配置项。
4. page:accordion.variant(enum flush | card,默认 flush)
读点:containers.tsx:734 — schema?.variant ?? schema?.properties?.variant ?? 'flush';消费点 containers.tsx:735-736 决定每个面板的 itemClass(flush = border-b last:border-b-0,card = 交给内部内容自己给边框),渲染结果肉眼可辨。
渲染器自己的注释原文:Authors opt in by setting variant: 'card' (or properties.variant: 'card') on the schema.
spec 侧 PageAccordionProps(packages/spec/src/ui/component.zod.ts:511-521)只有 items / allowMultiple / aria。
建议声明:variant: z.enum(['flush','card']).default('flush')。
5. page:tabs.tabStyle —— 这条和上面四条不同,请单独看
读点:containers.tsx:381 — const type: 'line' | 'card' | 'pill' = schema?.properties?.type || schema?.tabStyle || 'line';
spec 已经声明了这个概念,拼法是 type (PageTabsProps.type,enum line/card/pill,default line)。所以这不是「漏声明」,是「同一语义两种拼法」,而且两种拼法都被渲染器读 ,spec 的那种优先。
为什么 objectui 会另造一个拼法,不是随手漂移,而是载体冲突:
packages/react/src/SchemaRenderer.tsx:251-270 故意不把 properties.type / properties.id 上提到节点上 ,否则会遮蔽组件分发键;它的注释点名说的就是页签视觉风格这个 case。
packages/sdui-parser/src/validate.ts:20-30 的 BASE_PROPS 含 'type',所以一个名叫 type 的 manifest input 在扁平 SDUI 节点上永远不会被校验 (直接当基础 prop 跳过)。
扁平 / JSX 载体里节点长成 { type: 'page:tabs', items: [...], tabStyle: 'card' } —— type 是标签名,spec 的那个拼法在这个载体里根本无法表达 ;tabStyle 是扁平载体唯一能写的拼法。
因此 objectui 侧两条本地动作都被证否:撤掉 tabStyle 会删掉扁平载体唯一能表达的拼法;改成发布 type 会发布一个自家 parser 结构上无法校验的键。剩下的杠杆只在 spec 这一侧。
这条留一个 open question 给本仓决定,objectui 侧不猜 :
按 objectui 的判定框架(长期架构健康 + 让 AI 写的元数据难写错)两个轴看,方案 A 都更优:AI 作者面对「一个概念一种拼法、写错当场被 spec 按名拒绝」比面对「两种拼法都收、其中一种在某些载体里静默失效」出错率低得多。但这是本仓的契约决定,objectui 侧不代拍。
已经不需要做的一半:element:record_picker(#5775 已解决)
objectui#3797 正文把 element:record_picker 的 labelField / valueField / label 也列为 OFF-SPEC,并猜 labelField 是 displayField 的别名漂移。核验结论:猜对了一半,而且本仓早就修完了 —— packages/spec/src/ui/component.zod.ts:715-786(origin/main)现在把 labelField / valueField / label / sort / limit / emptyText 全部声明好,displayField / searchFields / multiple 转成了 retiredKey() 墓碑(#5775 / ADR-0087 D2),收敛方向正是 objectui 渲染器实读的 labelField。
objectui 侧那 3 条 flag 纯粹是 pin 落后造成的 :npm 上 @objectstack/spec 最新已发布版本是 17.0.0-rc.5,早于 #5775 。所以本单对 record picker 没有任何请求 ;objectui 的门里给这 3 条挂的豁免理由写的就是「#5775 已解决,等 objectui 的 spec pin 升上来后必须删掉这条豁免」,并且门有一条「豁免过期即失败」的断言,pin 一升上来这 3 条豁免就会红,强制清掉。
建议的验收
PageHeaderProps 增 recordChrome / showStar / showCopyId 三个 optional boolean(带默认值与 description);PageAccordionProps 增 variant。
page:tabs 按上面选定的方案落地(A 需要 conversion 条目 + 墓碑 + migration 说明)。
packages/lint/src/authoring-rules.ts:564-566 那段「the rest of the keys the renderers honour are declared」按实际情况改写,并把本单挂进 validateComponentProps 升 error 的前置清单(与 component.zod.ts 的 I18nLabelSchema 键声明为纯字符串,但 3 个平台页在 31 处授权内联多语言 map #5728 并列)。
发布后 objectui 侧升 pin,门里对应的豁免条目会自动失效(过期即红),由 objectui 分片 PM 派单清理。
关联:objectui#3797、objectui#3407 / objectui PR #3795 (单块版门)、objectui#3226 / 本仓 #4827 (同族的 page-header 别名收窄)、#5068 、#5775 、#5728 、#5435 、ADR-0087 D2、ADR-0049
Part of objectstack-ai/objectui#3797
按跨分片移交协议从 objectui 分片转来:
packages/spec是共享契约面,objectui 侧不自行修改(objectui AGENTS.md #0 / #0.1)。objectui 那一侧只落门(全仓 registryinputs×ComponentPropsMapparity 测试 + 显式豁免名单),这 5 个 prop 在门里挂本单号等上游。inputs与 specComponentPropsMap之间没有 parity 门,4 个 block 发布了 pin 版 spec 不接受的顶层 input objectui#3797(objectui 分片 PM 已判级bug+pm:queue,已派发实施)session_01GTRjn8xBqp75dk7kFupVRtorigin/main@ea1d9165d8d4b5c5d806e095f9604cb3731d4e62;objectuiorigin/main@c4c0ac8972c2efe69b11997dade1cc7655c15666(pin@objectstack/spec@^17.0.0-rc.5,npm 上最新的已发布版本)机制:两个平台权威今天对同一个键公开互相矛盾
objectui 的 registry
inputs是对外发布的授权面,不是文档:packages/sdui-parser/scripts/gen-manifest.ts把它序列化进sdui.manifest.json(save-gate + parser 白名单)和sdui-intrinsics.d.ts(JSX 授权类型面);packages/components/src/renderers/layout/page.tsx:462另外用getKnownTypes()在运行时现搭一份 JSX 页面校验 manifest。而自 #5068 起,
packages/lint/src/validate-component-props.ts按typedispatch,用lintUnknownKeysAgainstSchema判properties的未声明键,os validate/os build/os lint都跑。于是下面这 5 个键今天的处境是:
.d.ts告诉作者(尤其是 AI 作者)这个键合法;validateComponentProps对同一个键报warning:未声明;三者不可能同时对。这正是 #5435「平台权威不得指向自己闸门会拒绝的键」的场景,只是这一次错的一半在 spec 的声明面。
顺带修正一处上游注释的事实:
packages/lint/src/authoring-rules.ts:564-566现在写着「the rest of the keys the renderers honour are declared」这句不成立 —— 下面 5 个键渲染器 honour、spec 未声明。因为
validateComponentProps升 error 的前置条件写的是「warning-period inventory is empty」,这 5 条就是那份 inventory 里还没被记进去的部分,直接卡在升级路上。清单(objectui 侧 file:line 证据,均以 objectui
origin/main为准)渲染器读点全部在
packages/components/src/renderers/layout/containers.tsx。这些读的都是schema.*/schema.properties.*,也就是作者写的页面元数据;该文件真正由宿主注入的东西走 RecordContext(headerSystemActions/isFavorite/onToggleFavorite),并且故意没有出现在inputs里 —— 所以下面 5 条不是「renderer-only 内部 prop」,是作者配置项。1.
page:header.recordChrome(boolean,默认 true)containers.tsx:979—schema?.recordChrome === false || schema?.properties?.recordChrome === false;消费点containers.tsx:1453(注释原文:Author hasn't opted out via recordChrome: false)决定走「记录 chip 版页头」还是「裸 h1 版页头」。apps/console/src/preview-samples.ts:68—{ type: 'page:header', properties: { title: 'Welcome to the CRM', recordChrome: false } },而这条恰好是validateComponentProps今天会报 warning 的形状;packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:413-414也在每个合成的记录页页头上产出它。recordChrome: z.boolean().default(true),描述写「false 时不渲染记录 chip,退回裸 h1 页头(非记录页用)」。2.
page:header.showStar(boolean,默认 true)containers.tsx:980;消费点containers.tsx:1531→RecordTitleChip showStar(packages/components/src/custom/RecordTitleChip.tsx)。3.
page:header.showCopyId(boolean,默认 true)containers.tsx:981;消费点containers.tsx:1532→RecordTitleChip showCopyId(RecordTitleChip.tsx:47/64/114)。4.
page:accordion.variant(enumflush|card,默认flush)containers.tsx:734—schema?.variant ?? schema?.properties?.variant ?? 'flush';消费点containers.tsx:735-736决定每个面板的itemClass(flush=border-b last:border-b-0,card= 交给内部内容自己给边框),渲染结果肉眼可辨。Authors opt in by setting variant: 'card' (or properties.variant: 'card') on the schema.PageAccordionProps(packages/spec/src/ui/component.zod.ts:511-521)只有items/allowMultiple/aria。variant: z.enum(['flush','card']).default('flush')。5.
page:tabs.tabStyle—— 这条和上面四条不同,请单独看containers.tsx:381—const type: 'line' | 'card' | 'pill' = schema?.properties?.type || schema?.tabStyle || 'line';type(PageTabsProps.type,enum line/card/pill,default line)。所以这不是「漏声明」,是「同一语义两种拼法」,而且两种拼法都被渲染器读,spec 的那种优先。packages/react/src/SchemaRenderer.tsx:251-270故意不把properties.type/properties.id上提到节点上,否则会遮蔽组件分发键;它的注释点名说的就是页签视觉风格这个 case。packages/sdui-parser/src/validate.ts:20-30的BASE_PROPS含'type',所以一个名叫type的 manifest input 在扁平 SDUI 节点上永远不会被校验(直接当基础 prop 跳过)。{ type: 'page:tabs', items: [...], tabStyle: 'card' }——type是标签名,spec 的那个拼法在这个载体里根本无法表达;tabStyle是扁平载体唯一能写的拼法。tabStyle会删掉扁平载体唯一能表达的拼法;改成发布type会发布一个自家 parser 结构上无法校验的键。剩下的杠杆只在 spec 这一侧。这条留一个 open question 给本仓决定,objectui 侧不猜:
PageTabsProps.type重命名为tabStyle,配 ADR-0087 D2 conversion 条目(加载时properties.type→properties.tabStyle),旧拼法转retiredKey()墓碑。理由:与 SDUI props 声明与 renderer 不一致:6 处「renderer 兑现但 ComponentPropsMap 未声明」+ 2 处「声明了没人读」(#5068 error 升级的 spec 侧前置) #5775 处理displayField→labelField完全同形 —— 往「渲染器实读的拼法」收敛,而不是往「声明得好看的拼法」收敛;并且顺手消掉SchemaRenderer的type特例与BASE_PROPS的结构性盲区(一个 props 键叫type、而它所在节点的判别键也叫type,在任何扁平载体里都不可授权,这本身就是个设计事故)。代价:一次 breaking + 一条 conversion,已发布页面里写properties.type的要靠 conversion 改写。tabStyle作为type的别名。 代价:一个语义两个拼法永久固化,正是 Prime Directive Add comprehensive test suite for Zod schema validation #12 / objectui AGENTS.md #0.1 要防的第二方言,而且 SDUI props 声明与 renderer 不一致:6 处「renderer 兑现但 ComponentPropsMap 未声明」+ 2 处「声明了没人读」(#5068 error 升级的 spec 侧前置) #5775 刚为同一类问题选了「收敛到一种」。不推荐。按 objectui 的判定框架(长期架构健康 + 让 AI 写的元数据难写错)两个轴看,方案 A 都更优:AI 作者面对「一个概念一种拼法、写错当场被 spec 按名拒绝」比面对「两种拼法都收、其中一种在某些载体里静默失效」出错率低得多。但这是本仓的契约决定,objectui 侧不代拍。
已经不需要做的一半:
element:record_picker(#5775 已解决)objectui#3797 正文把
element:record_picker的labelField/valueField/label也列为 OFF-SPEC,并猜labelField是displayField的别名漂移。核验结论:猜对了一半,而且本仓早就修完了 ——packages/spec/src/ui/component.zod.ts:715-786(origin/main)现在把labelField/valueField/label/sort/limit/emptyText全部声明好,displayField/searchFields/multiple转成了retiredKey()墓碑(#5775 / ADR-0087 D2),收敛方向正是 objectui 渲染器实读的labelField。objectui 侧那 3 条 flag 纯粹是 pin 落后造成的:npm 上
@objectstack/spec最新已发布版本是17.0.0-rc.5,早于 #5775。所以本单对 record picker 没有任何请求;objectui 的门里给这 3 条挂的豁免理由写的就是「#5775 已解决,等 objectui 的 spec pin 升上来后必须删掉这条豁免」,并且门有一条「豁免过期即失败」的断言,pin 一升上来这 3 条豁免就会红,强制清掉。建议的验收
PageHeaderProps增recordChrome/showStar/showCopyId三个 optional boolean(带默认值与 description);PageAccordionProps增variant。page:tabs按上面选定的方案落地(A 需要 conversion 条目 + 墓碑 + migration 说明)。packages/lint/src/authoring-rules.ts:564-566那段「the rest of the keys the renderers honour are declared」按实际情况改写,并把本单挂进validateComponentProps升 error 的前置清单(与 component.zod.ts 的 I18nLabelSchema 键声明为纯字符串,但 3 个平台页在 31 处授权内联多语言 map #5728 并列)。关联:objectui#3797、objectui#3407 / objectui PR #3795(单块版门)、objectui#3226 / 本仓 #4827(同族的
page-header别名收窄)、#5068、#5775、#5728、#5435、ADR-0087 D2、ADR-0049