feat(types,sdui-parser): ComponentInput.type 可声明联合,五个标本停止对合法写入告警 (#3832) - #4975
Conversation
`ComponentInput.type` 只能承载一个粗类型,而 spec 有一批键接受多种形状。 声明只能挑一条臂,于是本仓自己的 manifest 门就对另一条臂的合法值报 `type-mismatch` —— 其中四个标本是最刺眼的形状:input 自己的 description 教作者写内联翻译映射,同一个 input 的 `type: 'string'` 又让 `checkType` 对这个映射告警。severity 是 warning,所以页面照样编译渲染,代价是正确 写入上的噪音会训练作者(含 AI 作者)忽略真的 `unknown-prop` / `type-mismatch`。 按 2026-08-09 维护者裁定方向 (a):`type` 除单个粗类型外还接受粗类型 **数组**,任一臂命中即过。三处声明位(types 的 base.ts / plugin-scope.ts 与 core 注册表自己的那份)同移,`ComponentInputSchema` 同步收紧为 非空、互异的臂数组 —— 空臂表或重复臂在创作期就被拒,而不是被悄悄归一化。 五处声明改为契约的真实形状,这五条合法写入上的 `type-mismatch` 消失: `page:header.title` / `.subtitle`、`page:card.title`(字符串或内联翻译 映射,对 rc.6 的 `ComponentPropsMap` 实测,渲染器两条臂都过 `pickLocalized`)、`record:alert.title` / `.body`(同两种形状;固定的 spec 不带 `record:alert` props schema,故依据渲染器)、 `element:text_input.defaultValue`(`string | number`,原先窄化为 `'string'` 并只在 description 里点出 number 臂)。 向后兼容:单字符串形态保持合法且仍是单臂键的规范拼法 —— 校验行为逐字 不变(单臂诊断连 `invalid-enum` 及其 error severity 都是字节相同), `manifestFromConfigs` 把单元素数组折回裸字符串,所以已发布 `sdui.manifest.json` 里的既有条目序列化结果不变,数组只出现在真正声明了 联合的地方。`generateDts` 同 PR 跟进,联合输入产出 TS 联合类型,作者 type-check 的 .d.ts 与门接受的形状一致。 联合放宽的是「什么算合法」,不是把检查关掉:不命中任何一条臂照旧报告, 多臂不命中按最严的那条臂的 severity 报告(含 `enum` 臂时为 error), 并且臂要对齐契约而非放宽门 —— `element:text_input.defaultValue` 故意 不加 `object` 臂(spec 在该键上拒绝映射),`element:record_picker.emptyText` 保留单 `'string'` 臂(该渲染器丢掉映射形态,objectui#4163)。 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
合并顺序提示(与 PR #4967 / #3809 的一处文本重叠) 两个 PR 都动
两处在同一个 JSDoc 列表里且只隔两行,所以后合的那个必然遇到冲突,而且冲突长得像「一段散文改了两次」,很容易被顺手取一边。正确的解法是两半都要:
我这边不做规避性改动(把这条留在原文=在仓里留一句本 PR 刚证伪的话)。PM 决定合并顺序即可,先合哪个都行。 Generated by Claude Code |
…ponentinput-union
|
已 merge 首轮 CI(head 三条红逐条归因,全部跨三个本 PR 一行都没碰的包:
判据(不是推断):这三个文件在 merge 之前的本分支上全绿 —— **顺带一个真问题被这次 merge 暴露出来并已处理:**本 PR 的 spec 侧证据原本测在 所以五处声明的臂在两个 pin 上都正确, merged tree 上的完整重验证: merge 冲突:无(main 的改动集中在 package.json / lockfile / 另外几个包,与本 PR 的 22 个文件不相交)。与 PR #4967 在 Generated by Claude Code |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
PM 验收 ✅ ACCEPT(#3832,批次 19 —— 维护者 2026-08-09 裁定方向 (a) 的实施件) 实物核验(按真实 merge-base 对账):22 files,+944/−118 —— 编码选 CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。 反向验证读数:①预判「正向断言空绿、红在对照」实测命中(9 红全落对照与机制面)—— 对照断言不是装饰的实测依据;②2624 条既有断言零变化钉住向后兼容;③b 假臂全绿如实报告并单独立 #4971,没有顺手把门禁塞进本 PR。 #4970( Generated by Claude Code |
Resolve the one header-narrative conflict in registry-inputs-spec-parity.test.ts: keep this branch's rewritten types bullet (expressiveness closed by #3832, the comparison half tracked as #4971) and drop this branch's pre-#3809 tombstone bullet — main already records that limit as closed (#3809 landed via #4967), so the LIMIT list stays at exactly the two items its preamble announces. Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Fixes #3832
按 2026-08-09 维护者裁定方向 (a):
ComponentInput.type学会表达联合,checkType任一臂命中即过,三处声明位同移,消费者同 PR 跟进,五个实测标本改声明真实联合。前提验证(先做,后写码)
卡正文给的行号已漂移,先重定位并实测:
1b21b1aaa)packages/types/src/base.ts:215:357(接口)/:366(type那一行)packages/types/src/plugin-scope.ts:172:170/:172validate.ts:106-142checkTypecontainers.tsx:1584/:703:1696/:753卡正文说「两声明位」,实际是三处 —— 第三处
packages/core/src/registry/Registry.ts:14才是所有组件注册真正import type { ComponentInput }的那一份。漏掉它的话五个标本连编译都过不去。另外packages/types/src/zod/base.zod.ts的ComponentInputSchema是这个契约被强制的那一半,也必须同步。四处都改了,结构副本本身另立 finding(见下)。五个标本的当前告警,用 app 真正搭 manifest 的那对函数(
manifestFromConfigs+validateTree,与page.tsx:462的kind:'jsx'编译同一条路)实测:前提成立(
premise_still_valid: true),六个键全部复现。「五个标本」= 卡里那张表的五行(第三行
record:alert.title/.body是一行两个键),共六个键。spec 侧同时实测(
ComponentPropsMap,pin 在@objectstack/spec17.0.0-rc.6):page:header.title/.subtitle/page:card.title—string=OK i18n-map=OK number=no boolean=no,即 string-or-record 联合;element:text_input.defaultValue—string=OK number=OK i18n-map=no boolean=no;record:alert—ComponentPropsMap里没有这个块,所以这两个键的第二条臂不是对 spec 得来的,是对渲染器得来的:renderers/record-alert.tsx:126-127两个键都过pickLocalized。这一点在代码注释里写明了,不装作有 spec 依据。编码选择:
type收数组,而不是新增types?: ControlType[]裁定把编码留给实施者,硬约束是「既有单字符串形态保持合法」。两个选项都满足,选
type: ControlType | ControlType[],理由:types?:会让同一件事有两处拼法,并且可以互相矛盾(type: 'string', types: ['number']语法完全合法)—— 要么再加一条「types必须包含type」的规则去缝,要么留一个二义的发布面。这正是 AGENTS.md #0.1 反对的第二种方言,只不过这次两种方言在同一个对象里。types?:下最自然的写法是两处都写(冗余),而冗余的两处一定会漂。types?:是新增一个已发布字段,老消费者读type会静默拿到「一条臂」,永远看不见另一条 —— 与今天的错误行为完全一样,只是从此变成设计。数组形态下老消费者的switch (input.type)拿到数组会掉进default,行为是「不报告」而不是「报错」——所以我把每一个读点都找出来改了(见下),而不是指望类型报错;这一点在变异 ① 里被实测钉住了。三条约束把「向后兼容扩展」做成可测的事实:
invalid-enum及其errorseverity 也不变);manifestFromConfigs把单元素数组折回裸字符串,所以已发布sdui.manifest.json里每一个既有条目序列化结果不变,数组只出现在真正声明了联合的地方(实测:全公开面共 5 个联合条目,就是那五个公开层标本键,其余一个不动);联合放宽的是「什么算合法」,不是把检查关掉:不命中任何一条臂照旧报告;多臂不命中时按最严的那条臂的 severity 报告(含
enum臂 →error,否则warning),这样「在 enum 旁边加一条臂」不能把 enum 的封闭表降级成可忽略的 warning;code 用type-mismatch(报告的事实是「不命中任何已声明的臂」),消息里带上 allowed 值,作者仍看得到那张表。消费点清单
按「谁读
ComponentInput.type/ManifestInput.type」扫出来的,不按包猜。packages/sdui-parser/src/validate.tscheckTypearmAccepts/armExpectation,任一臂命中即过;'slot'与词表外的臂照旧接受一切(保住原default: return null分支)packages/sdui-parser/src/index.tsmanifestFromConfigsINPUT_TYPES.has(i.type) ? i.type : 'string'换成canonicalizeInputType(折叠单臂、去重、保序、丢弃词表外臂)packages/sdui-parser/src/codegen.tsgenerateDts(emitInterface的 slot 过滤 +tsType)i.type !== 'slot'改成「有没有非 slot 臂」(旧写法会让['slot','string']漏过过滤,再被tsType的 default 兜成string)packages/sdui-parser/src/codegen.tsgenerateBlockListname/required/bindingpackages/sdui-parser/src/types.tsManifestInput.typepackages/sdui-parser/src/index.tsRegistryConfigLike.inputs[].typestring→ `stringpackages/types/src/base.tsComponentInput.typeComponentInputControlType(臂词表,唯一一份)packages/types/src/plugin-scope.tsComponentInput.typebase.ts,不再复制那 11 个字面量packages/core/src/registry/Registry.tsComponentInput.typepackages/types/src/zod/base.zod.tsComponentInputSchema.typeComponentInputControlTypeSchemapackages/components/src/renderers/layout/page.tsx:464(kind:'jsx'页面编译)meta?.inputs原样交给manifestFromConfigs,归一化在 #2 完成packages/sdui-parser/scripts/gen-manifest.ts/scripts/dump-public-manifest.mjsgetPublicConfigs()→manifestFromConfigs→ 写文件plugin-designer的PropertyEditor/PageDesigner/ReportDesigner/ProcessDesigner/DataModelDesigner)PropertyEditor有自己的词表('text' | 'number' | 'boolean' | 'select' | 'color' | 'textarea',与粗类型词表不是一个东西),PageDesigner的propertyFields是硬编码的label/x/y/width/height。全仓\.inputs\b扫描下,designer 侧没有任何一处读注册的inputs—— 也就是说裁定里点名的「designer 面板」在今天的代码里根本没有连上这个字段。这本身是另一个形状的问题(设计器不消费发布面),不在本卡范围packages/types/src/widget.tsWidgetInput.typecheckType/ manifest;结果是 widget 面现在比组件面窄一条臂,记进 finding #4972发布产物没有需要重新生成的已提交基线:
sdui.manifest.json/sdui-intrinsics.d.ts/sdui-blocks.md全部是构建期由浏览器 dump 产出的,仓库里不存在提交版本(已确认)。产物变化面实测:公开面 5 个条目获得数组形态,PageHeaderProps/PageCardProps/RecordAlertProps三个接口的相应键变成string | Record< string, unknown >,其余逐字不变。五标本前后告警对照
page:header.title['string','object']warning/type-mismatch … expected a stringtitle={true}→ 照旧type-mismatchpage:header.subtitle['string','object']subtitle={42}→ 照旧报page:card.title['string','object']title={['Account']}→ 照旧报record:alert.title['string','object']title={7}→ 照旧报record:alert.body['string','object']element:text_input.defaultValue['string','number']warning/type-mismatch … expected a string42,尤其配inputType: 'number')defaultValue={true}与defaultValue={{ en: … }}→ 都照旧报(spec 在这个键上拒绝映射,所以故意不给object臂)臂要对齐契约,不是用来放宽门的。同族里刻意不动的一个:
element:record_picker.emptyText保留单'string'臂 —— 契约确实是联合,但该渲染器把值直接塞进文本节点、不做 locale 解析(#4163),声明一条渲染器丢掉的臂等于广告一个到不了屏幕的形状。它原来的注释把窄化归因于「类型表达不了」,那条理由现在不成立了,已改写成真正的理由;标本测试里也为它留了一条反向断言。测试
新增两个文件,分工不同:
packages/sdui-parser/src/__tests__/input-type-union.test.ts—— 机制面(任一臂、对照、消息、最严 severity、slot、单臂逐字不变、canonicalizeInputType的折叠/去重/丢弃、generateDts的联合产出、整条 compile 管线);apps/console/src/__tests__/component-input-union-specimens.test.ts—— 用户可见验收面,走 console 真正注册的 registry。每个标本都配一条对照断言(不命中任何臂必须仍然报告),这不是装饰:变异 ① 实测出来,只撤回 checkType 的任一臂逻辑时,正向断言会因为什么都没产出而继续绿,红的只有对照。改动的既有 fixture,逐个判处置(不是批量重拼):
packages/components/src/__tests__/text-input-inputs-spec-parity.test.ts—— 整条替换。它原来钉的就是我删掉的那条肢:expect(input('defaultValue')?.type).toBe('string'),注释里写着这是有意的窄化。把它改指向新值只会钉住声明、而放掉它存在的理由(两条臂必须就是 spec 接受的那两种),所以改成把臂当集合、对 spec 的safeParse裁决逐条比对 —— 任一侧动了都红。packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts、apps/console/src/__tests__/registry-inputs-spec-parity.test.ts文件头、packages/layout/src/index.ts注释 —— 只改叙述:这三处都把「ComponentInput.type表达不了联合」当事实在讲,断言不受影响。console 那条尤其重要:它列的是这个门不检查什么,表达力这一半现在关掉了,而比对那一半没有,新的真相与 finding [finding] 声明的粗类型「臂」没有任何门禁与 spec 比对 —— #3832 让臂可写之后,一条假臂能让 manifest 门放行 spec 拒绝的值,且全绿 #4971 一起写在那里。按规则的消费半径扫 fixture(不按包扫):
manifestFromConfigs/validateTree/generateDts的调用点共 21 个文件,跨packages/sdui-parser、packages/components、packages/layout、packages/plugin-charts、apps/console、examples/schema-catalog、scripts/,全部跑过。三条变异记录(先书面预判,后跑;变异前已 commit,还原用
git checkout)① 把
checkType的任一臂逻辑撤回单臂 switch(保留数组声明)。预判(刻意不是「前绿后红」):正向断言会继续绿,而且是空绿 ——
switch (input.type)拿到数组不匹配任何case,掉进default: return null,那个 prop 什么都不产出;红的应该是对照断言。实测:9 条红。四个标本的失败点逐个落在对照行(
specimens.test.ts:104/:122/:142/:164),报的都是AssertionError: expected [] to include 'type-mismatch'—— 空数组,即什么都没产出。机制面同时红在「不命中任何臂仍报告」「消息列出每条臂」「一个 prop 一条诊断」「含 enum 臂时最严 severity」「compile 管线」。预判命中,这也正是「对照断言不是装饰」的实测依据。② 向后兼容对照:把五处标本声明还原成单字符串,保留新代码。
预判:全部既有测试保持绿(单字符串形态在新代码下零行为变化),只有本卡新增的臂断言红。
实测:
Test Files 2 failed | 117 passed,Tests 5 failed | 2624 passed—— 红的 5 条全部是本卡的臂断言(4 条标本 + 改写后的 text-input 那条)。2624 条既有断言无一变化,其中包含所有老 manifest fixture:examples/schema-catalog的 pageheader 门、packages/layout的invalid-enumseverity 钉子(#3985)、sdui-parser的 compile / tier / render、components 的各 parity 测试。预判命中。③ 给标本声明一条假臂(spec 不接受的 type)。两半答案不同,如实记录。
预判:「联合不是放行一切」这半成立(不命中任何臂照旧报告);但「臂对齐契约」这半今天没有门禁,所以有 per-block spec 断言的键会红、没有的键会全绿。
实测:
element:text_input.defaultValue加'object'臂 → 红,expected [ 'number', 'object', 'string' ] to deeply equal [ 'number', 'string' ](本 PR 改写的那条 per-block 断言把臂当集合对 spec 比对)。page:card.title加'number'臂 → 全绿(Test Files 93 passed, Tests 856 passed)。同一次变异下门的行为:string/i18n map/number都 CLEAN(数字标题被放行,而 spec 拒绝它),boolean与array照旧type-mismatch … expected a string or an object or a number。即联合确实不是放行一切,但「臂是否对齐契约」靠的是 per-block 纪律而不是门禁。预判命中,并且这半个空缺单独立了 finding #4971 —— 没有塞进本 PR 顺手扩围。
changeset
.changeset/component-input-type-union-arms.md,types/core/sdui-parser/components/plugin-detail全部 minor。发布面扩展(不是破坏):单字符串形态保持合法、诊断逐字不变、单臂序列化字节相同 —— 按 AGENTS.md「objectui 不声明 major」的固定组规则,扩展记 minor(与 #3985 同档)。packages/layout只动了一行注释,不单独记 bump。顺手发现的相邻缺陷(都立卡,不并入本 PR)
element:text.content/element:button.label是 #3832 那张表漏掉的第 6、7 个同形标本 —— 同一个矛盾,今天照样报type-mismatch#4970 —element:text.content/element:button.label是同一个矛盾的第 6、7 个标本(都在PUBLIC_BLOCKS,渲染器都过pickLocalized,spec 都接受映射),实测今天照样报type-mismatch。裁定逐字写的是「the five measured specimens」,这两个不在那张表里,所以按不扩围单独立卡;机制已经落地,各是一行。[finding]— 声明的臂没有任何门禁与 spec 比对(变异 ③ 的实测就是判据)。ComponentInput有三份结构副本(types 两份 + core 一份)外加一个窄孪生WidgetInput,而 #4580 的裁定说的是「re-export 而不是 restate」—— 副本之间今天已经有一处分歧 #4972[finding]—ComponentInput有三份结构副本 + 一个窄孪生WidgetInput,而 finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580 的裁定说的是 re-export 而不是 restate;副本之间今天已有一处休眠分歧(min/max/step/placeholder只在 types 那份上)。本 PR 已把臂词表收敛成单一声明,其余键待判。PR #3795 的 member-shape 姊妹题没有并入(
ComponentInput依然是扁平的,数组/对象 input 的成员形状仍只能写在 description 里)—— 那是另一维,本卡只动同一层的类型本身。草稿状态,等 PM 验收后再 undraft。
Generated by Claude Code