Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

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.5 的 packages/spec/src/ui/component.zod.ts 里 retiredKey 出现 0 次(实测 grep -c),所以 ComponentPropsMap 的 31 个条目现在一个墓碑都没有,门当前的每一条判定都是对的。这也是本单不排队的理由。

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

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

  • PageCardProps.body —— objectstack#5775 把它退役了,收敛到 children(理由:body 是「其他所有容器都叫 children 的那个组合槽」的第二种拼法,而渲染器两个都读)。
  • 本仓 page:card 的 inputs(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:card 的 inputs 在发布一个 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_picker 的 displayField / searchFields / multiple 同样是墓碑,不过本仓不声明它们,所以不产生假绿。

建议的落地

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

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

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

  2. page:card 的 body → children 逐块处置:按 registry inputs 与 spec ComponentPropsMap 之间没有 parity 门,4 个 block 发布了 pin 版 spec 不接受的顶层 input #3797 的同一套两向判定做。注意渲染器两个都读,body 优先,所以撤声明面(改成 children)不动运行时,是和 PR fix(layout): page-header 的 registration inputs 不再宣告 description #3265 (page-header 的 description) 完全同形的一步;运行时那条 ?? 什么时候能删,取决于上游 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

Activity

  1. yinlianghui commented on Aug 8, 2026

    @yinlianghui
    ContributorAuthor

    补一条:同一个盲区在反方向的症状是假红,不是假绿 —— #3808 的实施(把反方向门补进 registry-inputs-spec-parity.test.ts)撞上了这一点,顺手记在这里,免得本单落地时只修了一半。

    两个方向,同一个 Object.keys(shape),相反的失效

    本单描述的是正方向(「不得声明 spec 不接受的键」):墓碑留在 shape 里 → 门认为「spec 接受」→ 假绿。

    反方向(「spec 声明的键必须可从 inputs 发现」)用的是同一个集合的补集,于是墓碑的效果整个翻过来:

    正方向 反方向
    判据 declaredInputs - Object.keys(shape) Object.keys(shape) - declaredInputs
    墓碑落在 「spec 接受」一侧 「spec 声明」一侧
    失效 假绿:声明了一个按名被拒的键,门不报 假红:门要求本仓去声明一个按名被拒的键

    假红的具体后果比漏报更难缠:pin 一升,门会要求 element:record_picker 声明 displayField / searchFields / multiple,而照做的话正方向立刻红(声明了 spec 不接受的键)。两个方向互相打架,而且两边都"按门的要求"做了。

    #3808 里怎么处理的

    没有在那一单里改 specTopLevelKeys(那是本单的落点,且今天 pin 版零墓碑,现在写就是没人知道已失效的死码 —— 正是本单「建议的落地」第 1 条末尾自己提醒的那件事)。做法是:

    • 三个 record_picker 墓碑键进反方向的显式豁免名单,理由里写明它们不会随 pin 升级自动过期,只会随本单落地而过期;
    • 门的文件头 LIMIT 段落把两个方向的相反症状并列写出,并指名本单是一处修两向的落点(narrow specTopLevelKeys,两个方向共用)。

    顺带纠正 #3808 正文的一处错误:它的 C 类理由写着这三个键「pin 升上来后会从 spec 的接受集消失」。按本单的机制这是错的 —— ADR-0087 D2 的退役是替换成 z.never().optional(),不是删条目,所以它们不会从 Object.keys(shape) 里消失。#3808 的豁免注释已按本单的描述改正。

    对本单落地的一条建议

    本单第 1 条建议加一条「墓碑确实还留在 shape 里」的前提断言。反方向让这条断言更划算:它同时钉住两个方向的推导前提,而且一旦上游哪天改成真删键,两边的墓碑识别代码会一起被判定为失效死码,而不是只有正方向那一半。

    page:card.body 这个标本也有反方向的一面:本仓 page:card 的 inputs 声明 body 不声明 children,pin 升上来后 —— 正方向对 body 假绿(本单已述),反方向对 children 报真红(spec 新接受的 children 本仓确实没发布)。所以那次 pin 升级时,page:card 会同时是一个假绿和一个真红,处置(body → children)刚好一次解决两个。

    参考:apps/console/src/__tests__/registry-inputs-spec-parity.test.ts —— 反方向的门与 UNPUBLISHED_EXEMPTIONS(#3808)


    Generated by Claude Code

  2. claude commented on Aug 8, 2026

    @claude
    Contributor

    Findings round: PROMOTED — finding → pm:queue.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. yinlianghui commented on Aug 16, 2026

    @yinlianghui
    ContributorAuthor

    【范围扩注】座位 repo:objectui(session_01GTRjn8xBqp75dk7kFupVRt,2026-08-16,来自 #3829 的测量):本卡现有修法只点名 console gate 的 specTopLevelKeys —— 实测发现同一盲点还有第二处:packages/layout/src/__tests__/page-header-authorable-keys.test.tsx:58 同样用 Object.keys(SpecPageHeaderProps.shape) 判 authorable,墓碑(z.never().optional())留在 shape 里使它对已退役键假绿。实施本卡时该文件应入范围(或复用同一 tombstone 识别 helper)。另:#3829 落地后,console gate 的 retired-upstream 类 exemption 将累计六条等本卡收割(record_picker 三连、page:card.body、record:details.layout、page:header.icon+page:card.actions)—— 落地时一并删。


    Generated by Claude Code

  4. self-assigned this
    on Aug 17, 2026
  5. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    认领(objectui 车道分片 PM,会话 session_01GTRjn8xBqp75dk7kFupVRt,批次 19):branch claude/issue-3809-tombstone-aware-parity。范围 = 本卡 + 08-08 反方向评论 + 08-16 范围扩注:墓碑识别(z.never() 基)做成共享 helper,console gate 的 specTopLevelKeys 两方向共用收窄 + packages/layout 的 page-header-authorable-keys 同盲点一并修;加「墓碑确实还留在 shape 里」前提断言(两方向推导前提同钉);#3808 的三条 record_picker 显式豁免与 #3829 后累计的六条 retired-upstream 豁免一并收割删除。page:card.body 迁移本身不在本卡。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions