Skip to content

test(layout): pin the README Registration key list to registerLayout() (#4860) - #4893

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4860-readme-registry-pin
Aug 17, 2026
Merged

test(layout): pin the README Registration key list to registerLayout() (#4860)#4893
yinlianghui merged 2 commits into
mainfrom
claude/issue-4860-readme-registry-pin

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4860

背景:三处同源键表,只有这一处没有钉子

@object-ui/layout 的组件键表在仓内发布在三个地方,其中两处早已机械钉到源码:

位置钉子
content/docs/guide/layout.md 的 "registers five keys …" 句guide-layout-sidebar-nav-doc.test.ts(#4840)
packages/layout/src/index.ts 键的存废app-shell-not-a-component-key.test.tsx(#4841)+ side-effects-manifest.test.ts(只有数量下限)
packages/layout/README.md## Registration

这个不对称是实测出来的,不是推断:#4841 撤销 app-shell 注册时,guide 的键表被钉子打红、逼出了文档修改;README 的键表列着同一个已撤销的键,没有任何测试发现,最后是 PR #4859 里人工改对的。「有人读到了」不是机制。README 是本包的 npm 首屏 —— 列一个没人注册的键,等于读者照着写那个节点、拿到渲染器的红框 Unknown component type(OBJUI-001)。

改动

只加两个文件,README.md 本体零修改(自 PR #4859 起它就是对的):

  • packages/layout/src/__tests__/readme-registration-keys.test.ts — 新钉子
  • .changeset/layout-readme-registration-keys-pin.md — 空 frontmatter(纯测试,不发布任何包)

钉子形态:双向

#4840 钉 guide 的形态,但拆成两个方向、两条独立诊断,因为它们是两种不同的缺陷:

  1. README 多列 → 教了一个不解析的节点类型([finding] app-shell 注册后在 schema 路径上不可用:四个 ReactNode 插槽 JSON 一个都填不了,inputs 又为空,节点解析得到却永远渲染不出壳 #4841 的形状;更早是 packages/layout: sideEffects: false contradicts the load-time registerLayout() — a side-effect-only import can be tree-shaken away #3899 的幽灵 sidebar-nav)。
  2. README 少列 → npm 首屏藏起一个活的可编写面;而一个哪里都没写的键,接下来就是被当成死代码撤掉。

第三条测试把下限断言成显式事实:两个集合差在两个空列表上都是平凡绿,所以正则一旦不再匹配(README 改写、注册调用改用双引号),上面两条会静默变成空操作。这条同时挡住重复项 —— 重复项能让两个集合差都为空,而两份列表其实并不相同。

顺序不比较:列表恰好按注册顺序排列是好习惯、不是契约;一个因重排就变红的钉子,读者会学会绕开它。

期望值一律从源码读出,绝不写死

在测试里硬编码这五个键,正是这个钉子要防的缺陷本身:#3899 自己的散文就自信地列了一个本包从未注册过的 sidebar-nav。所以两侧每次运行都重新解析 —— 红了永远是「改 README(或改 barrel)」,绝不是「更新测试」。

这个解析有一个值得写下来的依赖:源码侧读的是 ComponentRegistry + .register( 的正则,因此 src/index.ts 注释里逐字引用一个 register 调用会被当成活注册。index.ts 用散文描述那个已撤销的 app-shell 调用正是为此(#4841 留下的注释),本钉子是第三个受益者,文件头注释里也记了这条。

为什么是新文件,而不是扩 readme-app-shell-example.test.ts

那个文件(#4817)自述的扫描面是 AppShell 开标签与 AppShellProps 表格,且只读 README 与 AppShell.tsx。Registration 键表是关于 src/index.ts 的事实 —— 本包已有的 README 钉子从不打开这个源。按包内既有惯例(readme-sidebar-nav-example.test.ts / readme-app-shell-example.test.ts),一个被记录的关注点配一个钉子文件。

两处与派单预判不同的形态,记录在案

  1. README 侧不是 markdown 表格。 派单预判需要解析表格行;实际上 Registration 键表是一句散文里括号内的反引号列表(README.md:26-27),而且跨行折行。所以解析照 content/docs/guide/layout.md 的 SidebarNav 两块教一个从未注册的 sidebar-nav JSON 节点,键表也与 SidebarNavProps/NavItem 全面不符 #4840 的 guide 句子做(锚点词之间用 \s+,列表体用 [^)]*),而不是照表格行做 —— guide 那条钉子正是被一次纯重排打红过,才留下这个教训。
  2. 共用 helper 没有提取。[finding] packages/layout/README.md 的注册键表是三处同源键表里唯一没有钉子的一处 —— #4841 撤 app-shell 时它是唯一没被打红的 #4860 把它列为选项 A 的附带项。这条正则在仓内已有三份副本(guide-layout-sidebar-nav-doc.test.tsapp-shell-not-a-component-key.test.tsxside-effects-manifest.test.ts),本 PR 是第四份。刻意不提取:为服务一个钉子去改三个钉子文件,而这些文件是故意自包含的 —— 一个从共享模块导入自己读取器的钉子,可以在它所守护的文件之外被静默架空。理由写在新文件的头注释里。

反向验证(两个方向,先预判后跑)

预判写在跑之前:方向 A 只让第 1 条红、方向 B 只让第 2 条红,且两次 packages/layout 其余 16 个测试文件全绿 —— 后者才是本单前提的实测证据。

方向 A —— README 键表加一个假键 fake-key:

× names no key `registerLayout()` does not register
AssertionError: The `## Registration` list in packages/layout/README.md names a component key that
`registerLayout()` does not register. This README is the npm landing page for @object-ui/layout …
Registered: page-header, page:card, responsive-grid, navigation-renderer, app-schema-renderer
: expected [ 'fake-key' ] to deeply equal []
Test Files 1 failed | 16 passed (17)
Tests 1 failed | 183 passed (184)

方向 B —— README 键表删掉活键 app-schema-renderer:

× and names every key `registerLayout()` does register
AssertionError: packages/layout/src/index.ts registers a component key the `## Registration` list in
packages/layout/README.md does not name …
Registered: page-header, page:card, responsive-grid, navigation-renderer, app-schema-renderer
Documented: page-header, page:card, responsive-grid, navigation-renderer
: expected [ 'app-schema-renderer' ] to deeply equal []
Test Files 1 failed | 16 passed (17)
Tests 1 failed | 183 passed (184)

两次都与预判逐字相符,而且两次其余 16 个文件全绿 —— 即在本 PR 之前,README 键表向任一方向漂移,仓内没有任何东西会发现。这正是 #4860 立单所依据的事实,现在被测量下来了。

还原用 git checkout -- packages/layout/README.md,未用 git stash(stash 栈跨 worktree 共享,AGENTS.md 明令禁止)。

验证

还原后全绿:

$ pnpm exec vitest run packages/layout --maxWorkers=2
Test Files 17 passed (17)
Tests 184 passed (184)
$ pnpm --filter @object-ui/layout type-check # tsc --noEmit && tsc -p tsconfig.test.json
(无输出,exit 0)
$ pnpm --filter @object-ui/layout lint
✖ 41 problems (0 errors, 41 warnings) # 全部是既有告警,新文件零告警
$ node scripts/check-control-bytes.mjs
✅ check-control-bytes: OK (scanned 4371 tracked text file(s); skipped 85 binary).
$ node scripts/check-changeset-presence.mjs --base origin/main
✅ 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
Every one of them has an EMPTY frontmatter — declared as releasing nothing …
$ node scripts/check-changeset-no-major.mjs
✅ No changeset declares a `major` bump.

type-check 用的是包级脚本而非仓根 turbo run type-check 全量:改动是 packages/layout 内一个新增测试文件,不被任何包导入,能编译它的正是该包的 tsconfig.test.json(已跑,干净);容器内多 agent 并行,全量 type-check 属于无谓的重扫。


Generated by Claude Code

#4860)
The same component-key list is published in three places. Two were held to
source -- the guide's sentence by guide-layout-sidebar-nav-doc.test.ts
(#4840) and the keys' existence by app-shell-not-a-component-key.test.tsx
(#4841) -- and packages/layout/README.md's `## Registration` paragraph was
not. Measured asymmetry: #4841 deregistered `app-shell`, the guide's list
went red and forced the page to change, the README's list named the same
retired key and nothing noticed.
Both directions, each with its own diagnostic: a key the README names but
the barrel does not register (a reader authors it and gets the OBJUI-001
`Unknown component type` panel), and a key the barrel registers that the
README does not name (a live authoring surface nobody can discover). Both
lists are parsed on every run -- hardcoding them here would reproduce
#3899's own defect, whose prose listed a `sidebar-nav` key this package has
never registered.
A third test asserts the floors as a visible fact: two set differences are
both trivially green on two empty lists, so a regex that stops matching
would silently turn the pin into a no-op.
README.md itself is unchanged -- its list has been correct since PR #4859.
Co-authored-by: Claude <noreply@anthropic.com>
Empty frontmatter: test-only, no published source and no behaviour change.
The gate guards `src/**`, and a new pin under `src/__tests__/` lands there,
so the declaration is what answers it.
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-DW9vXhq4.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)498.36KB111.03KB
core (index.js)3.79KB1.52KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.05KB43.28KB
fields (index.js)231.73KB57.60KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.86KB10.83KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.14KB32.58KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.81KB59.97KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)115.34KB27.74KB
plugin-gantt (index.js)164.06KB39.94KB
plugin-grid (index.js)192.23KB51.30KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.23KB26.97KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.70KB11.19KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收:ACCEPT(#4860,批次 15,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

实物核验(已过)

  • 纯新增两文件:packages/layout/src/__tests__/readme-registration-keys.test.ts(231 行)+ 空 frontmatter changeset;README 本体零改动 —— 钉子钉的是现状,不顺手改被钉物,方向正确。
  • diff --stat 与 dev 报告对账一致;模型标识 grep 0;content/docs/releases/** 零触碰;边界外文件零触碰。

测试设计

  • 双向钉:README 散文键表 ↔ 注册表实际键,两个方向各自断言(README 有而注册表无 → 红;注册表有而 README 无 → 红),并带非空、无重复下限断言,防解析器静默解析出空集变假绿。
  • README 散文括号列表的解析形态(括号内逗号分隔键名)由测试自带解析器覆盖,解析规则与断言在同一文件内可审。
  • 三处有据偏离均在 PR 描述公开披露并附论证,PM 逐条核对通过。

反向验证

  • 双向各做一次变异证负:两个方向的人工制造缺口均被逐字命中报红,还原后绿 —— 钉子两面都真咬。
  • dev 仅跑包级 type-check(资源纪律,全量交 CI)—— CI 已补齐全量答案。

CI(亲读终态):19 项 check runs 全 completed,17 success + 2 skipped(path-filter 计绿),零失败。

附带产出:新 finding #4894 已立卡入池。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 02:41
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit c29e3ccAug 17, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4860-readme-registry-pin branch August 17, 2026 02:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude