Skip to content

fix(cli): objectui check 的 knownTypes 改由注册表推导,消灭手写副本的双向漂移 - #5128

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5115-check-knowntypes-drift
Aug 18, 2026
Merged

fix(cli): objectui check 的 knownTypes 改由注册表推导,消灭手写副本的双向漂移#5128
yinlianghui merged 1 commit into
mainfrom
claude/issue-5115-check-knowntypes-drift

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #5115

基线 origin/main = 8378e9954

前提复核:卡面四条逐条成立,并且比卡面更宽

卡面判断 复核结果
knownTypes 是手写副本,已漂移 成立。packages/cli/src/commands/check.ts:35-39(卡面写 34-38,内容逐字一致)
crud 在列表内但全仓零渲染器 成立。机器推导 + 邻近词反查均零命中
校验绿灯而运行时落占位符 成立,两半都实测(见下)
CRUDSchema 四声明面俱在,独缺渲染器 成立。interface types/src/crud.ts:425、zod types/src/zod/crud.zod.ts:160、validator core/src/validation/schema-validator.ts:135、builder core/src/builder/schema-builder.ts:170

卡面留了一句「crud 未必是唯一一个 —— 反向也成立」。用 deriveRegistryKeys(#5094 席先例)对全仓 register 现场做机器推导后,两个方向都被证实,且第二个幽灵确实存在:

  • 幽灵 2 个:crudgallerygallery 卡面没点名 —— 真身拼法是 object-gallery(plugin-list/src/ObjectGallery.tsx:609)。
  • 缺失 221 个 bare 键 + 全部 namespaced 拼法:object-gridobject-formcarddivview:grid 全被误报为未知。

实测证据

objectui check(真实构建产物,探针目录)修复:

⚠️ Unknown schema type "view:grid" in my-nsgrid.json     # 真键,误报
⚠️ Unknown schema type "object-grid" in my-grid.json     # 真键,误报
⚠️ Unknown schema type "totally-made-up-xyz" in my-bogus.json
✓ All checks passed          # type:"crud" 一声不吭

同一批 schema 交给真实 SchemaRenderer 渲染(happy-dom 吞 console,读数写文件):

crud: unknownPanel=true    len=395
gallery: unknownPanel=true len=401
page: unknownPanel=false   len=161

门禁说 OK 的类型,运行时确实不存在 —— 卡面第 3 条两半闭合。

使用面(修剪的前提门):零

type: "crud"全仓 JSON/YAML 中零命中,而 check() 只判 .json(file.endsWith('.json'))。仅有的三处出现在 markdown(README.md:138,265content/docs/api/schema-reference.md:530),不在 check 的扫描面上。故机械修剪成立,不会让任何现存 schema 从绿变红。gallery 同(type: 'gallery' 的两处出现是视图种类词汇表,非组件键)。

方向选择:为什么是「生成 + 双向钉」而不是「运行时查注册表」

先侦查包边,再定形。(a) 运行时从 ComponentRegistry 推导,实测不可行,两个独立原因:

  1. packages/cli 只依赖 @object-ui/components。旧列表里 15 个真实注册项中,11 个(kanban calendar dashboard chart detail timeline gantt map object-view detail-view object-chart)由 CLI 并不依赖的插件包注册。够到它们意味着一个为了判断一个字符串而拖进 React / recharts / maplibre 和二十个兄弟包的 CLI —— 正是 gridFilterInputSpelling 先例拒绝的那种包边。
  2. 就算买了也答错题。已发布的 CLI 跑在用户工程里,本仓无从得知对方装了哪些插件。这条 check 能诚实做出的断言是「这个名字指向 ObjectUI 栈注册过的组件」,那是关于源码的断言,与 CLI 自己进程里恰好加载了什么无关。

于是取 (b),但把「副本」变成生成物:

  • packages/cli/src/utils/known-schema-types.ts — 生成文件,659 个键(bare + namespaced)。
  • scripts/regenerate-known-schema-types.mjs — 生成器,复用 deriveRegistryKeys(不新写第二个扫描器),带 --check 模式。
  • scripts/__tests__/known-schema-types-derivation-5115.test.ts提取式双向钉,两侧都活读源码,不写化石键表。

bare 与 namespaced 两种拼法都收,因为 Registry.register 自己就存两个键(register('grid', C, { namespace: 'view' })view:grid bare grid,除非 skipFallback)。推导结果整体收下、不做过滤 —— 过滤规则会是第二个要保持诚实的东西,而 check 的问题(「这名字指向注册过的东西吗」)对所有形状答案一致。

反向验证(先书面预判,commit 后变异还原)

变异 预判 实测 是否相符
(a) 把 'crud' 塞回列表 幽灵断言 + 可重生成断言 + crud 告警测试红 5 红,含预判的 3 条 相符,多 2 条
(b) 注册表侧改键 只有幽灵方向红 两向都红 不符,已如实记录
(b′) 列表侧删 'object-grid' 「omits no registered type」红,幽灵断言绿 正是如此(missing = ['object-grid']) 相符

(a) 多出的 2 条:一条是 isKnownSchemaType('crud') 的定点钉(精神上在预判内);另一条「sorted and de-duplicated」是我把 'crud' 插在 'container' 之后所致(context-menu 排在两者之间),属插入位置的产物,不是 crud 本身的语义,据实记下以免下个读者误读。

(b) 预判不符,原因值得记:派发单预判「注册表侧假删一个真键 → 钉子从另一向红」。实测两向同时红。原因是 namespaced 注册成对产出键:把 object-view 改名,推导同时失去 object-view + plugin-view:object-view获得 改名后的 namespaced 键。第二次我特意改成一个推导里已存在的键(object-grid)想构造「纯丢失」,仍然两向红 —— 断言输出直接给出原因:

phantoms: [ 'object-view', ...(1) ]
missing:  [ 'plugin-view:object-grid' ]

也就是说:只要注册表带 namespace,任何改键动作都不可能是单向的。真正单向检验「omits no registered type」的唯一办法是从列表侧删(即 (b′))。派发单的方向预设在这里是反的,我按实测记录而非套模板。

(c) 自选方向 —— 校验器对 namespaced 拼法的处理。预判:纯集合成员判定,不对 namespace:type 做结构解析,故不会回退到 bare 半边。实测(真实构建产物)完全相符:

schema type 修复前 修复后
crud 静默(缺陷本体) 告警
gallery 静默 告警
object-grid 误报 静默
view:grid 误报 静默
plugin-view:object-view 误报 静默
bogus:grid 告警 告警(不回退 bare 半边)
totally-made-up-xyz 告警 告警

#5094 席「量过的门只判 bare」的先例对照:这里两种拼法都是一等公民,因为推导两种都产出,与 Registry 自身的双键存储同形。#5106 记的「门从不判 namespace 半边」在文档门上仍然成立,本 PR 不触及那扇门。

行为变化(changeset 已如实写明)

crud / gallery 从绿变(告警),大量真实组件类型从误报变绿。告警始终只是告警 —— 退出码依旧只由 JSON 解析失败驱动(实测 EXIT=0),故没有任何之前能过的运行会因此失败。

#4631 的边界

#4631同一个 component type 的三个声明面(TS schema type / registry meta inputs / renderer prop reads)互不一致,且无人调和。本 PR 讲的是第四份、位于 CLI 的手写副本与注册表的分歧 —— 是「有没有这个类型」的存在性问题,不是「这个类型的键读法一不一致」的形状问题。两者不重叠:本 PR 一行未动任何渲染器、CRUDSchema、builder 或 registry meta。crud 渲染器缺席的去留(补渲染器 / 走 ADR-0049 退役)仍是另案,本单按派发边界不裁。

验证

pnpm exec vitest run packages/cli                      => 7 files, 149 tests passed
pnpm exec vitest run scripts/__tests__                 => 53 files, 1219 tests passed
pnpm --filter @object-ui/cli type-check                => 通过
pnpm type-check:scripts                                => 通过
pnpm type-check                                        => 81 successful, 81 total
pnpm --filter @object-ui/cli lint                      => 0 errors(11 个既有 any warning,均在他文件)
node scripts/check-doc-component-types.mjs             => 绿(659 registered keys)
node scripts/regenerate-known-schema-types.mjs --check  => OK
node scripts/check-control-bytes.mjs                   => OK

控制字节:check-control-bytes 绿,但自扫另有收获 —— 我写 ANSI 剥色正则时,编辑工具把一个裸 ESC 字节(十六进制 1b)实体化进了测试文件,而该门不扫 ESC(与 #5157 的 0x01 同形的盲区)。源码里已改写成反斜杠-u-001b 的转义形(不落裸字节);grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 对全部改动文件 0 命中。

同一个坑在本 PR 正文上又复发了一次:初版正文里那句话直接贴了转义序列本身,GitHub 存下来的是一个裸 ESC 字节。现已改成用文字描述而非粘贴 —— 写「关于控制字符」的文字时,描述它,不要贴它。

半径外发现(均未夹带进本 PR)

`objectui check` 用一个手写的十七项数组判定 schema 的 `type` 是否已知,
没有任何东西让它与 ComponentRegistry 保持一致。在 8378e99 上实测,
它同时朝两个方向错了:

- 幽灵项两个:`crud`、`gallery` 在列表内,全仓零注册。
  `{ "type": "crud" }` 过 check 静默绿灯,同一份 schema 交给
  SchemaRenderer 却渲染出 OBJUI-001 "Unknown component type" 占位符
  —— 两半都已实测。`CRUDSchema` 的 interface / zod / validator 分支 /
  builder 四面俱在,独缺注册;`gallery` 的真实拼法是 `object-gallery`。
- 缺失项 221 个 bare 键,外加全部 namespaced 拼法:`object-grid`、
  `object-form`、`card`、`div`、`view:grid` 全被误报为未知。这个量级的
  误报不是外观问题 —— 它训练作者忽略输出,于是幽灵方向失去唯一的读者。

列表移到 packages/cli/src/utils/known-schema-types.ts,由
`node scripts/regenerate-known-schema-types.mjs` 从 deriveRegistryKeys
生成(与判定文档片段的是同一份推导,不新写第二个扫描器),并由
scripts/__tests__/known-schema-types-derivation-5115.test.ts 双向钉住:
注册表有而列表无 → 红,列表有而注册表无 → 红。bare 与 namespaced 两种
拼法都收,因为 register('grid', C, { namespace: 'view' }) 确实两个键都存。

运行时查 ComponentRegistry 的方案已侦查并否决:十五个真实注册项里有
十一个来自 CLI 并不依赖的插件包,且已发布的 CLI 跑在用户工程里,
本仓无从得知对方装了哪些插件。

行为变化双向,但警告仍只是警告 —— 退出码依旧只由 JSON 解析失败驱动,
之前能过的运行不会因此变红。check() 另加可选的扫描目录入参
(默认仍为 process.cwd()),使行为可对夹具树测试。

Fixes #5115

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 25.3 KB 350 KB
Entry file index-y8mMGMRm.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.79KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.71KB 1.22KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.19KB 112.21KB
core (index.js) 4.11KB 1.62KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.03KB 44.08KB
fields (index.js) 234.25KB 58.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 39.16KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 241.22KB 60.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 123.33KB 29.93KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.97KB 53.15KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.17KB 26.99KB
plugin-map (index.js) 18.72KB 6.09KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM 验收:ACCEPT(session_01GTRjn8xBqp75dk7kFupVRt,objectui 分片)

实物核验:merge-base 8378e9954;head 8539d02d8;6 files, +1177/−9;标识 grep msg/diff 双零,trailer 唯一正确;releases 与在飞席位文件(RelatedList/SchemaRenderer/skills)零触碰;控制字节自扫含 ESC 位零命中(针对本席发现的 check-control-bytes 不扫 0x1b 盲区,PM 加扫复核)✅。CI 亲读:18/20 终态零失败(shard 1/3/4、Type Check、Lint 全绿),仅 shard 2 与 Build & E2E 在跑且零失败信号,本地对应面已直接覆盖(cli 149 + scripts 1219)—— 按 PR #5105/#5108 口径交 auto-merge 门。

验收要点:

  1. 方向选择的包边论证是本单最硬的一格:15 个注册项中 11 个由 CLI 不依赖的插件包注册,运行时查注册表 = 把 React/recharts/maplibre 拖进已发布 CLI,且用户工程里的插件集本就不可知 —— 「生成物 + regenerate --check + 提取式双向钉」是正确形,且复用 deriveRegistryKeys 不写第二个扫描器(单一事实源纪律)。
  2. 前提复测比卡面更宽(幽灵 crud+gallery、反向缺 221 bare 键 + 全部 namespaced),两半闭环实测(check 绿灯 EXIT=0 + SchemaRenderer unknownPanel=true);使用面前提门(type:"crud" 全仓 JSON/YAML 零命中、check 只扫 .json)使机械修剪安全 —— 修剪不会红到任何现存 schema。
  3. 反向验证 (b) 预判不符的如实报告有结构价值:namespaced 注册成对产出键 ⇒ 注册表侧改键必然双向红,单向检验只能从列表侧删((b′) 补构造)—— 这条修正了「双向钉各守一向」的朴素模型,记入案头。(c) 证实新列表对两种拼法一等公民,与 Registry 双键存储同形(区别于 content/docs/plugins/plugin-dashboard.mdx 的手动注册循环是 #5064 README 半的文档站镜像 —— 照抄注册 11 个幻影 type + 11 条弃用告警 #5094 席量过的「门只判 bare」)。
  4. ESC 字节的两次自捕(测试文件 + PR 正文)与 check-control-bytes 不扫 0x1b 的盲区记录 —— 记入案头;PR 正文 footer 被 update 端点剥除属已知平台行为。
  5. 新卡 objectui check 把每个 JSON 文件的根 type 都当成组件键判定,于是在任何 Node 工程里都对 package.json 的 "type": "module" 报未知类型 #5127(check 把 package.json 的 "type": "module" 报未知组件)PM 分诊:bug + needs-user-decision —— 误报是明确缺陷,但「哪些 JSON 算 SDUI schema」的判别机制(A/B/C)是行为面裁定,席位不自裁正确。finding(docs/gate): check-doc-component-types 的扫描面止于代码围栏 —— 八个 PR 新加的「真实 register 键表」全部在门外,且门从不判 namespace 半边 #5106 的补充事实(.md 不在扫描面且有真幽灵教学)已动摇其 observation 自评 —— 分诊轮升级时一并复核。
  6. check() 的可选扫描目录入参是可测性最小接缝(默认行为不变)—— 采信。

三件套照常:本评论 → undraft → auto-merge(SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 18, 2026 02:50
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit 68d9e28 Aug 18, 2026
20 of 21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5115-check-knowntypes-drift branch August 18, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants