发现于 objectui#3789 的测量阶段(退役 objectui PageHeader 的 subtitle ?? description 回退)。该单的门是「证明每一条 page-header 元数据路径都经过执行改写的 loader」。门没过,原因在上游:conversion 层的 page-component 走查面比同仓 lint 层的走查面窄一大截,page-header-subtitle-alias 因此改不到一批 spec-valid 的 header 节点。
两个走查器,覆盖面不同
|
位置 |
覆盖 |
| conversion 层 |
packages/spec/src/conversions/walk.ts:193 mapPageComponents |
只有 pages[].regions[].components[],一层,到此为止 |
| lint 层 |
packages/lint/src/page-walk.ts walkPageComponents |
regions[].components[] 加 page.slots.*、properties.children[]、properties.items[].children[](tabs/accordion)、properties.body[] / properties.footer[](card),深度递归 |
page-walk.ts 的模块注释把这件事说得很清楚:PageComponentSchema 是 .strict(),组件自身没有 children 键,子树都在无类型的 properties 包里,所以递归必须手写。lint 手写了;conversion 没有。walk.ts:222-236 把这个边界写成了刻意选择(「Region level is the whole surface a page-component conversion can reach」),但它成立的前提是「嵌套处的键会被 tombstone 在 parse 期挡掉」——description 不在 PageHeaderProps 里被 tombstone,而 properties 是自由 bag,所以什么都没挡。
实测(objectui 实装的 @objectstack/spec@17.0.0-rc.5)
applyConversionsToStoredItem('pages', row) + PageSchema.safeParse(row):
| 形状 |
spec-valid |
被 page-header-subtitle-alias 改写 |
regions[].components[].properties.description |
是 |
是(基线,正常) |
slots.header.properties.description(regions: []) |
是 |
否 |
regions[].components[] 里 page:card 的 properties.children[].properties.description |
是 |
否 |
第二行是最严重的一条:那正是 objectui 文档里推荐的定制 record 页头的写法(objectui content/docs/guide/slotted-pages.md,slot 表第一行 header → page:header,示例显式写 regions: []「slotted pages don't author regions」)。slotted 页把 regions 留空,mapPageComponents 于是一个节点都不访问。
复现片段(节点用 properties bag,因为 PageComponentSchema 是 strict,内联键会报 unrecognized_keys):
const slotted = {
name: 'account_detail_page', label: 'Account Detail',
type: 'record', object: 'account', kind: 'slotted',
regions: [],
slots: { header: { type: 'page:header', properties: { title: '{name}', description: 'x' } } },
};
PageSchema.safeParse(slotted).success // => true
applyConversionsToStoredItem('pages', slotted) // => properties.description 原样留存,零 notice
影响
page-header-subtitle-alias 的退役承诺兑现不了。 该条目的 doc 写「objectui#3226 deletes its ?? once this ships」,但 slots / 容器嵌套两处它改不到。objectui 侧删掉 subtitle ?? description 会让这些页面静默丢副标题(标题照渲染,第二行消失,不报错)—— 正是该条目选 conversion 而非 deletion 所要避免的那个失败形状。objectui#3789 据此停在 needs_decision。
- 不止这一条。 任何建在
mapPageComponents 上的 page-component conversion 都有同一个洞。其它几条靠 tombstone 兜底(嵌套授权点 tsc 会红),page-header-subtitle-alias 没有 tombstone 可靠 —— PageHeaderProps 里没有 description,而 properties 不按 type 校验,所以 properties.description 在任何位置都 parse 通过。
- 授权期也没有信号。
validateComponentProps 是 advisory + CLI_ONLY,且跑在 normalized 上(authoring-rules.ts:538-575 注释里明说:靠 conversion 先改写,所以改写过的别名「is never reported as undeclared」)。改不到的那些位置,conversion 不报、lint 不报(它虽然下钻得到,但 advisory 且只在 CLI)、schema 不报。三层皆无。
建议方向(留给 triage 定档)
倾向把 conversion 的走查面对齐 lint 已经写好的那一份 —— 让 mapPageComponents 复用 / 收敛到 page-walk.ts 同形的下钻(slots.* + properties.children / items[].children / body / footer),而不是在消费端加容忍。理由:
- 契约优先:声明的覆盖面应当等于渲染器实际服务的面。现在「同一个
description,放 region 一层就规范化、放 slot 里就不规范化」是位置决定语义,这条规则没人记得住。
- 让 AI 写的元数据难写错:slotted 页与容器嵌套恰恰是模型生成 page 元数据最常用的两种形状(
apps/console 的 preview-samples 注释自己也写了「these samples are copied — increasingly by models generating metadata」)。位置敏感的规范化是这类错误的藏身处。
- 该条目本身已按
PAGE_HEADER_COMPONENT_TYPES 做了 type gate,所以下钻不会碰到 element:text_input 那类另有真 description 的组件 —— walk.ts 划这条边界时担心的跨组件误伤,在这一条上已被 type gate 挡住。
另一条可行路线是把 description 在 PageHeaderProps 上做成 tombstone,让嵌套授权点在 parse 期被点名拒绝(与 record-picker 三键同构),但那会让 properties 需要按 type 真正校验,是更大的一步(与 #4001 相邻)。
Refs
objectui#3789(被本单阻塞的退役单)、objectui#3226(原裁定)、#4827(本 conversion 的登记单)、#5509、#5511(mapPageComponents 收敛单)、#5068 / #4001(properties 无门的历史)、ADR-0087 D2。
—— 由 objectui#3789 的测量阶段发现,未在该单改任何代码。
发现于 objectui#3789 的测量阶段(退役 objectui
PageHeader的subtitle ?? description回退)。该单的门是「证明每一条 page-header 元数据路径都经过执行改写的 loader」。门没过,原因在上游:conversion 层的 page-component 走查面比同仓 lint 层的走查面窄一大截,page-header-subtitle-alias因此改不到一批 spec-valid 的 header 节点。两个走查器,覆盖面不同
packages/spec/src/conversions/walk.ts:193mapPageComponentspages[].regions[].components[],一层,到此为止packages/lint/src/page-walk.tswalkPageComponentsregions[].components[]加page.slots.*、properties.children[]、properties.items[].children[](tabs/accordion)、properties.body[]/properties.footer[](card),深度递归page-walk.ts的模块注释把这件事说得很清楚:PageComponentSchema是.strict(),组件自身没有 children 键,子树都在无类型的properties包里,所以递归必须手写。lint 手写了;conversion 没有。walk.ts:222-236把这个边界写成了刻意选择(「Region level is the whole surface a page-component conversion can reach」),但它成立的前提是「嵌套处的键会被 tombstone 在 parse 期挡掉」——description不在PageHeaderProps里被 tombstone,而properties是自由 bag,所以什么都没挡。实测(objectui 实装的
@objectstack/spec@17.0.0-rc.5)applyConversionsToStoredItem('pages', row)+PageSchema.safeParse(row):page-header-subtitle-alias改写regions[].components[].properties.descriptionslots.header.properties.description(regions: [])regions[].components[]里page:card的properties.children[].properties.description第二行是最严重的一条:那正是 objectui 文档里推荐的定制 record 页头的写法(objectui
content/docs/guide/slotted-pages.md,slot 表第一行header→page:header,示例显式写regions: []「slotted pages don't author regions」)。slotted 页把regions留空,mapPageComponents于是一个节点都不访问。复现片段(节点用
propertiesbag,因为PageComponentSchema是 strict,内联键会报unrecognized_keys):影响
page-header-subtitle-alias的退役承诺兑现不了。 该条目的 doc 写「objectui#3226 deletes its??once this ships」,但 slots / 容器嵌套两处它改不到。objectui 侧删掉subtitle ?? description会让这些页面静默丢副标题(标题照渲染,第二行消失,不报错)—— 正是该条目选 conversion 而非 deletion 所要避免的那个失败形状。objectui#3789 据此停在 needs_decision。mapPageComponents上的 page-component conversion 都有同一个洞。其它几条靠 tombstone 兜底(嵌套授权点tsc会红),page-header-subtitle-alias没有 tombstone 可靠 ——PageHeaderProps里没有description,而properties不按type校验,所以properties.description在任何位置都 parse 通过。validateComponentProps是 advisory + CLI_ONLY,且跑在normalized上(authoring-rules.ts:538-575注释里明说:靠 conversion 先改写,所以改写过的别名「is never reported as undeclared」)。改不到的那些位置,conversion 不报、lint 不报(它虽然下钻得到,但 advisory 且只在 CLI)、schema 不报。三层皆无。建议方向(留给 triage 定档)
倾向把 conversion 的走查面对齐 lint 已经写好的那一份 —— 让
mapPageComponents复用 / 收敛到page-walk.ts同形的下钻(slots.*+properties.children/items[].children/body/footer),而不是在消费端加容忍。理由:description,放 region 一层就规范化、放 slot 里就不规范化」是位置决定语义,这条规则没人记得住。apps/console的 preview-samples 注释自己也写了「these samples are copied — increasingly by models generating metadata」)。位置敏感的规范化是这类错误的藏身处。PAGE_HEADER_COMPONENT_TYPES做了 type gate,所以下钻不会碰到element:text_input那类另有真description的组件 ——walk.ts划这条边界时担心的跨组件误伤,在这一条上已被 type gate 挡住。另一条可行路线是把
description在PageHeaderProps上做成 tombstone,让嵌套授权点在 parse 期被点名拒绝(与 record-picker 三键同构),但那会让properties需要按type真正校验,是更大的一步(与 #4001 相邻)。Refs
objectui#3789(被本单阻塞的退役单)、objectui#3226(原裁定)、#4827(本 conversion 的登记单)、#5509、#5511(
mapPageComponents收敛单)、#5068 / #4001(properties无门的历史)、ADR-0087 D2。—— 由 objectui#3789 的测量阶段发现,未在该单改任何代码。