Skip to content

parity 门只比键名,retiredKey() 墓碑会被判成「spec 接受」—— 今天 pin 版零墓碑所以休眠,spec pin 一升就是门里的假绿(page:card.body 是现成标本) #3809

Description

@yinlianghui

发现于 #3797 / PR #3806 的实施过程,是我自己刚落的那道门的一个盲区,写出来免得下一个人踩。今天不成立(见下),按 observation-class 只贴 finding、不排队,请 triage 决定级别 —— 但它成立的那一刻是门给出错误的绿,不是漏报一个键,所以别按「小事」放过。

机制

PR #3806 的门这样算「spec 接受的顶层键」:

Object.keys(ComponentPropsMap[type].shape)

@objectstack/spec 退役一个键的做法(ADR-0087 D2)不是从 shape 里删掉,而是换成墓碑 —— packages/spec/src/shared/retired-key.ts:

export function retiredKey(guidance: string) {
  return z.never({ error: () => guidance }).optional().describe(`[REMOVED] ${guidance}`);
}

墓碑仍然是 shape 里的一个条目,所以 Object.keys(shape) 照样包含它。于是一个被 spec 按名拒绝的键,在门看来是「已声明、接受」——declares no top-level input the spec does not accept 会绿。这是那道门唯一能给出的错误绿:它的失效方向本来只有漏报,加上墓碑就多了一个误判。

今天为什么不成立

pin 版 @objectstack/spec@17.0.0-rc.5packages/spec/src/ui/component.zod.tsretiredKey 出现 0 次(实测 grep -c),所以 ComponentPropsMap 的 31 个条目现在一个墓碑都没有,门当前的每一条判定都是对的。这也是本单不排队的理由。

什么时候成立,以及现成的标本

objectstack origin/main(@ ea1d9165d)那同一个文件里已经有 5 个以上墓碑,而其中一个正好压在本仓已发布的键上:

  • PageCardProps.body —— objectstack#5775 把它退役了,收敛到 children(理由:body 是「其他所有容器都叫 children 的那个组合槽」的第二种拼法,而渲染器两个都读)。
  • 本仓 page:cardinputs(packages/components/src/renderers/layout/containers.tsx:703-706)声明的是 body,没有 children。渲染器 containers.tsx:678 读的是 schema?.body ?? schema?.children

所以 spec pin 从 17.0.0-rc.5 升上去的那一刻:

  1. page:cardinputs 在发布一个 spec 已按名拒绝的键;
  2. PR test(console): registry inputs 与 spec ComponentPropsMap 的 parity 门推到全仓,4 个 OFF-SPEC block 逐块判定 (#3797) #3806 的门会报绿,因为 body 还在 shape 里;
  3. objectstack 侧 validateComponentProps 会对写了 properties.body 的页面报 warning —— 又是「两个平台权威对同一个键互相矛盾」,正是 registry inputs 与 spec ComponentPropsMap 之间没有 parity 门,4 个 block 发布了 pin 版 spec 不接受的顶层 input #3797 要消掉的那件事,只是这次门看不见。

element:record_pickerdisplayField / searchFields / multiple 同样是墓碑,不过本仓不声明它们,所以不产生假绿。

建议的落地

  1. 门补墓碑识别:把「spec 接受的键」从「shape 的键名」收紧为「shape 里不是墓碑的键名」。两条可行的判据,建议用前者、后者兜底:

    • 结构判据:剥掉 .optional() 包装后 inner type 是 never;
    • 文本判据:.description[REMOVED] 开头(retiredKey 固定加这个前缀)。

    补完之后 page:card.body 会在 pin 升上来的同一次改动里立刻变红,而不是静默通过。顺带值得加一条断言把这个前提本身钉住(「墓碑确实还留在 shape 里」),否则哪天 spec 改成真删键,这段识别代码就成了没人知道已经失效的死码。

  2. page:cardbodychildren 逐块处置:按 registry inputs 与 spec ComponentPropsMap 之间没有 parity 门,4 个 block 发布了 pin 版 spec 不接受的顶层 input #3797 的同一套两向判定做。注意渲染器两个都读,body 优先,所以撤声明面(改成 children)不动运行时,是和 PR fix(layout): page-header 的 registration inputs 不再宣告 description #3265 (page-headerdescription) 完全同形的一步;运行时那条 ?? 什么时候能删,取决于上游 conversion 条目是否覆盖到位(参考 objectstack#6775 —— conversions:page-component 只走 regions[].components[],slots.* 与容器嵌套漏改)。

时序上 1 和 2 可以一起做,也可以 1 先落(它无条件成立,且能保证 2 一旦漏做就在 pin 升级时红)。

参考位置:

关联:#3797 / PR #3806、objectstack#5775、objectstack#6776、objectstack#6775、ADR-0087 D2

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions