test(console): registry inputs 与 spec ComponentPropsMap 的 parity 门推到全仓,4 个 OFF-SPEC block 逐块判定 (#3797) - #3806
Conversation
#3797) 把 PR #3795 在 `record:highlights` 上落的单块检查(「不得声明 spec 不接受的顶层 input」)推广到所有「`ComponentPropsMap` 有条目且本仓注册了非空 `inputs`」的 block, 期望值运行时从 spec 自身 shape 推导(不复述键表),现有偏离全部进**显式豁免名单**, 每条带理由 + 追踪单号。 门放在 `apps/console/src/__tests__/`,因为它需要的是产出发布物那份完整注册图: `dev/manifest-dump.tsx` 用 `src/register-plugins.ts` + `@object-ui/components` 搭 manifest,本文件就导入这一对(与隔壁 `public-contract.test.ts` 同法),不自己攒一份 会自说自话的清单。覆盖面不限于 public tier —— `renderers/layout/page.tsx:462` 用 `getKnownTypes()` 现搭运行时 JSX 页面校验 manifest,所以非 public 的 block(如 `element:record_picker`)的 `inputs` 同样是活的 prop 白名单。 四个 OFF-SPEC block 逐块以代码判定,结论都落在 spec 一侧,因此**本仓四个 block 的 `inputs` 一个字节都没动**: - `page:header` 的 `recordChrome`/`showStar`/`showCopyId` —— 渲染器实读 (`containers.tsx:979/980/981`),消费于 `:1453` 的布局分支与 `:1531/:1532` 的 RecordTitleChip;作者可达且非宿主注入(宿主注入走 RecordContext 且故意不声明), `preview-samples.ts:68` 与 `buildDefaultPageSchema.ts:413` 今天就在写。 (issue 猜「可能是 #3226/PR #3265 有意保留的 renderer-only prop」已证否:#3265 动的是 `packages/layout` 的遗留别名 `page-header`,canonical 这个没被碰过。) - `page:accordion.variant` —— 渲染器实读 `containers.tsx:734`,`:735` 据此改每个 面板的边框类,spec 完全没有对应键。 - `page:tabs.tabStyle` —— spec 已用 `type` 声明同一概念,所以是「一义两拼」;但两条 本地动作都被证否:撤掉它会删掉扁平 SDUI 载体唯一能表达的拼法 (`SchemaRenderer.tsx:251-270` 故意不上提 `properties.type`),改成发布 `type` 又 会声明一个自家 parser 结构上无法校验的键(`validate.ts` 的 `BASE_PROPS` 含 `'type'`)。收敛方向是 spec 侧的契约决定,不在这里猜。 - `element:record_picker` 的 `labelField`/`valueField`/`label` —— 上游 objectstack#5775 已经解决,且正是往本仓渲染器实读的 `labelField` 收敛 (`displayField`/`searchFields`/`multiple` 已转 `retiredKey()` 墓碑)。本仓这三条 flag 纯粹是 pin 落后:npm 上最新已发布的 `@objectstack/spec` 是 `17.0.0-rc.5`, 早于 #5775。 前三项转 objectstack#6776(跨仓转移协议,`pm:queue`,带 `Part of objectui#3797`); 八条豁免全部挂单号。豁免会自己过期:一旦 spec 声明了某个被豁免的键, `carries no stale exemption` 就红,强制删条目,名单不会烂成永久 allowlist。 Fixes #3797
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。 git 实物核验( 逐块判定全部采纳:四块结论均为「spec 补声明」方向、本仓 inputs 零字节改动 —— 与 #3226/#3265 误归因的证伪(那次动的是 packages/layout 遗留别名,canonical 验证质量:四条反向验证先判后跑全吻合,RV2 的「探针没造对与断言不管用输出同形」自误披露是方法库级的诚实记录;「行为不回归为空项」如实声明而非编造(inputs 与渲染逻辑零改动,无可验面)。19 项 CI 全部完成 0 失败(PM 独立复核)。 衍生三单:#3807(record:details sections 教退役形状,#3407 同族)、#3808(反方向 15 键未发布,已按 A/B/C 预分类)另行判级;#3809(门的墓碑盲区,pin 升级即醒)为 finding 在案 —— pin 提升单执行时须连带处理。 Generated by Claude Code |
targetVariable 是 spec 的 declarative hint,全仓零读点(grep 实测 0 命中)—— 发布还是不发布是个未定的判断题,不是漏声明
#3834
Fixes #3797
两半件:门无条件成立并已落地;4 个 OFF-SPEC block 逐块以
origin/main代码两向判定,结论全部落在 spec 一侧,因此本仓四个 block 的inputs一个字节都没动,转 objectstack 立单 + 挂显式豁免等上游。核验基线:objectui
origin/main@c4c0ac8972c2efe69b11997dade1cc7655c15666(pin@objectstack/spec@^17.0.0-rc.5,也是 npm 上最新的已发布版本);objectstackorigin/main@ea1d9165d8d4b5c5d806e095f9604cb3731d4e62(只读)。A. 全仓 parity 门
新增
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts。把 PR #3795 在record:highlights上落的单块检查(「declares no top-level input the spec does not accept」方向)推广到所有「ComponentPropsMap有条目且本仓注册了非空inputs」的 block。期望值运行时从 spec 自身 shape 推导(
specTopLevelKeys()读ComponentPropsMap[type]的.shape,兼容lazySchema()代理),不复述键表 —— spec 增删键时门跟着动,而不是静默放宽。放置位置及理由(也写进了测试头注):门需要的是产出发布物那一份完整注册图。
dev/manifest-dump.tsx用src/register-plugins.ts+@object-ui/components搭 manifest,本文件就导入这一对(与隔壁public-contract.test.ts同法),不自己攒一份会自说自话的清单。覆盖面不限于 public tier:packages/components/src/renderers/layout/page.tsx:462用getKnownTypes()在运行时现搭 JSX 页面校验 manifest,所以非 public 的 block(element:record_picker不在PUBLIC_BLOCKS里)的inputs同样是活的 prop 白名单。判定的 15 个 block(
EXPECTED_COVERED,精确列表而非toContain—— 危险方向是收缩):element:button/element:number/element:record_picker/element:text/element:text_input/page:accordion/page:card/page:header/page:tabs/record:activity/record:chatter/record:details/record:highlights/record:path/record:related_list。另有 10 个「有 spec 条目、已注册、inputs为空」的 block 单独钉住(EXPECTED_WITHOUT_INPUTS),这样「声明面凭空消失」和「声明面长出方言」一样可见。门共 6 类断言 / 21 个 test:
judges every spec-carried block…pins the spec-carried blocks that are registered with no inputsresolves a non-empty accepted key set for each covered block.shape读不到时,失败读起来是「修读取器」而不是「全仓回归」BLOCK declares no top-level input the spec does not accept(it.each,15 条)every exemption names a real declared input on a covered blockevery exemption states a reason and references a tracking issuecarries no stale exemption显式豁免名单(8 条,全部带理由 + 单号)
page:header.recordChromecontainers.tsx:979,:1453据此选裸 h1 布局;preview-samples.ts:68/buildDefaultPageSchema.ts:413今天就在写 → 等 objectstack#6776 补声明page:header.showStarcontainers.tsx:980,:1531传给 RecordTitleChip 关掉关注星 → objectstack#6776page:header.showCopyIdcontainers.tsx:981,:1532传给 RecordTitleChip 关掉复制 ID → objectstack#6776page:accordion.variantcontainers.tsx:734,:735据此改每个面板边框类;渲染器注释原文即写variant: 'card'是作者 opt-in → objectstack#6776page:tabs.tabStylecontainers.tsx:381,且是扁平 SDUI 载体唯一能表达的拼法(详见下表)→ objectstack#6776element:record_picker.labelFieldelement:record_picker.valueFieldelement:record_picker.label豁免的门槛写在代码注释里,不是「渲染器读它」——读它只说明这个键值得被声明在某处,不说明它值得被声明在契约拒绝它的地方。门槛是「该偏离已由一张具名的在办上游单持有」,因为
@objectstack/spec不从本仓改(AGENTS.md #0 / #0.1)。B. 4 个 OFF-SPEC block 逐块判定
三选一的输出:(1) renderer 真实读且用户可达 → 「spec 补声明」,本仓不动 spec,转 objectstack 立单 + 豁免;(2) renderer-only / 不读 → 撤下或留下并在 description 写明;(3) 两向拿不准 → needs_decision。
四块全部落 (1)。渲染器读点全在
packages/components/src/renderers/layout/containers.tsx,读的都是schema.*/schema.properties.*(作者写的页面元数据);该 block 真正由宿主注入的东西走 RecordContext(headerSystemActions/isFavorite/onToggleFavorite)且故意没有出现在inputs里 —— 所以这几条不是 renderer-only 内部 prop,是作者配置项。page:headerrecordChromecontainers.tsx:979;消费:1453(注释原文Author hasn't opted out via recordChrome: false)决定「记录 chip 版页头」还是「裸 h1 版页头」。仓内已有作者:apps/console/src/preview-samples.ts:68写properties: { title, recordChrome: false };packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:413-414在每个合成记录页页头产出它page:headershowStarcontainers.tsx:980;消费:1531→RecordTitleChip showStar(custom/RecordTitleChip.tsx)page:headershowCopyIdcontainers.tsx:981;消费:1532→RecordTitleChip showCopyId(RecordTitleChip.tsx:47/64/114)page:tabstabStylecontainers.tsx:381schema?.properties?.type || schema?.tabStyle || 'line'page:accordionvariantcontainers.tsx:734schema?.variant ?? schema?.properties?.variant ?? 'flush';消费:735-736决定每个面板itemClass(flush=border-b last:border-b-0,card= 交给内部内容自己给边框),渲染结果肉眼可辨;渲染器注释原文:Authors opt in by setting variant: 'card' … on the schemaelement:record_pickerlabelField/valueField/labelpage:tabs.tabStyle为什么不是「撤下」也不是 needs_decisionspec 已经声明了这个概念,拼法是
type(PageTabsProps.type,enum line/card/pill,default line),所以这不是漏声明,是「一义两拼」,而且两种拼法渲染器都读、spec 那种优先。看起来该按 #0.1 直接撤tabStyle,但两条本地动作都被证否:tabStyle会删掉扁平 SDUI 载体唯一能表达的拼法。packages/react/src/SchemaRenderer.tsx:251-270故意不把properties.type/properties.id上提到节点上(会遮蔽组件分发键,注释点名说的就是页签视觉风格这个 case);扁平节点长成{ type: 'page:tabs', items: [...], tabStyle: 'card' },那里type是标签名。type会声明一个自家 parser 结构上无法校验的键:packages/sdui-parser/src/validate.ts:20-30的BASE_PROPS含'type',任何节点上都被当基础 prop 跳过。所以剩下的杠杆只在 spec 侧,而「改名
type→tabStyle(照 objectstack#5775 处理displayField→labelField的同形做法,往渲染器实读的拼法收敛)」还是「spec 同时声明别名」是本仓不该猜的契约决定 —— 两方案连代价一起写进 objectstack#6776 交上游拍,本仓侧不因此停摆(门无条件成立,该 input 挂豁免)。element:record_picker:issue 的猜测对了一半,而且上游早修完了issue 猜
labelField是 specdisplayField的别名漂移。查 objectstack 侧 alias/conversion 登记(git -C /home/user/objectstack grep,origin/main)结论:packages/spec/src/ui/component.zod.ts:715-786现在把labelField/valueField/label(以及sort/limit/emptyText)全部声明好;displayField/searchFields/multiple转成了retiredKey()墓碑,由 objectstack#5775 / ADR-0087 D2 完成;labelField(renderers/basic/record-picker.tsx:80-81props.labelField ?? 'name'),packages/spec/src/conversions/registry.ts:4710-4740有displayField→labelField的 conversion 条目。所以本仓这三条 flag 纯粹是 pin 落后造成的:npm 上
@objectstack/spec最新已发布版本是17.0.0-rc.5,早于 #5775。两仓都没有活要干,只等 pin 升上来;carries no stale exemption会在那一刻把这三条豁免顶红,强制删掉。顺带修正 issue 正文一处误归因
issue 提示「
page:header的 inputs 在 #3226 / PR #3265 刚被收窄过,所以那三个可能是有意保留的 renderer-only prop」。核验为否:PR #3265(d2363e710)动的是packages/layout里的遗留 kebab 别名page-header(registerLayout()的descriptioninput,对应 objectstack#4827),packages/components里 canonical 的page:header那次完全没被碰过。没有任何「有意保留」的记录,这三条就是普通的未声明作者配置项 —— 也因此不适用record:activity.showSubscriptionToggle那条先例(那条是反方向:spec 声明了、渲染器不读,所以为保持双向 parity 留下并在 description 写明 KNOWN GAP)。一处主动没做的事,写出来供 review 推翻:没有在
showStar/showCopyId/tabStyle/variant的description里加「本键当前未被 spec 声明」之类的话。理由:那等于把一条上游缺陷的临时说明发布进sdui.manifest.json(AI 作者读的东西),objectstack#6776 一落地它就过期,而且没有任何门会强制删它。豁免名单里的理由才是有过期机制的那份记录。验证
门的反向验证(方向先判后跑,四条)
page:card(当前零 off-spec、零豁免)塞一个假 inputbogusOffSpecProbepage:card那条主断言红,点名该键page:tabs.tabStyle的豁免理由换成不含单号的一句话every exemption states a reason…红并点名该条page:card.title(spec 确实接受的键)加一条豁免,模拟「上游已声明」carries no stale exemption红并点名,page:card主断言保持绿import '../register-plugins',模拟注册图丢失record:*;test 总数从 21 掉到 15RV1 输出:
RV2 输出(注意 20 passed ——
page:tabs主断言仍绿):RV3 输出:
RV4 输出:
一次探针自身的失误,如实记下:RV2 第一次跑成了绿。原因不是断言弱,是探针建错了 —— 我只替换了理由字符串的前半句,尾巴上的
objectstack#6776还在,/#\d+/照样命中。把整条理由换成不含任何单号的一句后才拿到预期的红。留在这里是因为「探针没造对」和「断言不管用」在输出上长得一模一样,而结论完全相反。行为不回归
这一条在本 PR 里是空的,如实说明而不是编证据:四个 block 的
inputs一个字节都没动,containers.tsx/record-picker.tsx的渲染逻辑零改动(diff 只有一个新测试文件 + 一个 changeset),所以没有「撤下 input 后行为是否回归」可验。反向验证阶段临时塞进containers.tsx的探针已按原文件字节还原(git status干净,只余新增文件)。与 PR #3791 新落的页头 CEL 代码零冲突 —— 本 PR 不碰containers.tsx。为兜底仍跑了四个 block 的行为面 + PR #3795 的单块门 + PR #3265 的 layout 别名门,全绿:
受影响包 + 新门(仓库根,flock + 4096MB + maxWorkers=2)
先
pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build建依赖过滤集,防 TS2307 假红。新门单跑:
Test Files 1 passed (1) / Tests 21 passed (21)。type-check / lint / 仓库门
Changeset
.changeset/registry-inputs-spec-parity-gate-3797.md,空 frontmatter。依据:scripts/check-changeset-presence.mjs的守卫面是「fixed组里每个包的src/**」,@object-ui/console在fixed里且住在apps/console,该脚本头注明确写了「No carve-out for test files undersrc/—— 限于src/__tests__/**的改动由空 frontmatter 豁免一行答完」。它要的是一句声明而不是一次发版:本 PR 没有任何发布行为或授权面变化(四个 block 的inputs未动),所以不声明任何包。已跑该脚本确认按空 frontmatter 通过。关联
pm:queue/domain:spec/repo:objectui,带Part of objectstack-ai/objectui#3797;立前已按关键词 + 文件路径查重,零命中)。该单同时纠正了 objectstackpackages/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」前置上。displayField→labelField,ADR-0087 D2)。inputs声明readonly,使其进入 sdui.manifest.json(objectstack#5176 裁定 A 的 objectui 半边) #3407 / PR fix(plugin-detail): record:highlights 的 fields 声明补上 readonly,让 manifest 能被作者读到 (#3407) #3795(单块版门)、page-header与page:header对外声明了两套 authorable 键:descriptionvssubtitle,消费端用??兜底 #3226 / objectstack#4827(page-header别名收窄)、objectstack#5068 / #5435。ComponentInput的成员形状声明位(跨 types/core/sdui-parser),独立一单。测试头注的 LIMIT 段已把这条边界写明,避免「绿了就以为成员键也在管」。Generated by Claude Code