Skip to content

fix(plugin-detail): quick_actions 读 aria 契约拼写,并撤下不存在的 actionNames 回退承诺 (#4663) - #5133

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4663-quick-actions-aria
Aug 18, 2026
Merged

fix(plugin-detail): quick_actions 读 aria 契约拼写,并撤下不存在的 actionNames 回退承诺 (#4663)#5133
yinlianghui merged 1 commit into
mainfrom
claude/issue-4663-quick-actions-aria

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4663

同一组件的两处生产侧缺陷,按 PM 两点代裁执行。基线 958d7573ac2ae610f9f9db9c7f52cf13f28b9ac3

前提验证(实测,非复述)

卡面三条判断逐条复测,全部成立:

  1. 读点:record-quick-actions.tsx 工具条读 schema.aria?.label || 'Quick actions';AriaPropsSchemastrictObject,label 是其 alias 条目。实测 safeParse({ ariaLabel }) 通过,safeParse({ label }) 返回 unrecognized_keys 并点名 label(消息为 “Did you mean labelariaLabel?”)—— alias 是拒绝消息设施,不是接受面。渲染侧双向实测:spec-valid 拼写到达 renderer 后零读取(落到内建兜底),renderer 认的拼写恰是 spec 拒的。
  2. 旁路确实断:SchemaRendererresolveAriaProps扁平 schema.ariaLabel 并以组件 prop 注入 aria-label;本 renderer 的 splitDesigner 只保留 data-obj-id / data-obj-type / style,rest 从不使用。且 normalizeListViewSchema 的 aria fold 只在 list-view 路径上运行,不经过本组件 —— 嵌套袋读点是唯一活路径。
  3. 回退不存在:无 actionNames 且宿主不传 actions 时,namesToResolve 空 ⇒ needsLookup false ⇒ 元数据从不被查询 ⇒ actions = [] ⇒ 虚线占位符。新增回归测试用一个有货的元数据 provider 驱动(对象上声明了 record_header 位置的 action),断言 getItem 从未被调用、该 action 不出现;控制用例证明同一套接线在给出名字时照常交付。

代裁一:aria 读点

const ariaLabel = (schema.aria?.ariaLabel ?? schema.aria?.label) || 'Quick actions';

canonical 优先,legacy 仅为契约封闭前的存量文档保留,两者同现时 canonical 赢。

空串语义按同键既有惯例选,并偏离了代裁的字面写法(?? ?? ),理由如下:

  • canonical-vs-legacy 用 ?? —— 对齐 normalizeListViewSchema 的 aria fold(ARIA_KEY_ALIASES),它仅在 canonical 为 undefined 时才搬运 legacy 键,故一个已声明的 ariaLabel: '' 会遮蔽陈旧的 label;
  • 内建兜底用 || —— 对齐 ListView 自身读点(schema.aria?.ariaLabel ? { 'aria-label': … } : {}),它把空串视为「没有可访问名」。因 role="toolbar" 必须有名字,此处「没有名字」落到内建兜底,而非 ListView 的省略属性。

若照字面写成纯 ?? 链,aria: { ariaLabel: '' } 会渲染出 aria-label="" —— 一个有 toolbar 角色却没有可访问名的元素,比兜底更差。该差异已被变异实测量化(见下 M3)。

⛔ 未动 spec,⛔ 未在本单声明 aria: AriaPropsSchema

代裁二:注册描述

actionNames 描述改为实测行为,不预告回退:

'Action names to expose, in order — resolved from the actions declared on the object. With no names (and no host-supplied actions) nothing is looked up and the bar renders its empty placeholder'

注册 inputs 是发布面(序列化进 sdui.manifest.json 与 JSX 授权类型),原句已流入工具链 —— objectstack#8744 的派发提示逐字引用它作为待验证声明,正是那条未通过验证。实现该回退属能力扩张,按分诊口径不做、另立卡。

objectstack#8744 follow-up 解锁状态

卡面写明「declares aria the day the renderer reads the contract spelling」。该条件现已满足:renderer 读 aria.ariaLabel,RecordQuickActionsProps 可以声明 aria: AriaPropsSchema 而不再是 declared-but-unenforced(#8691 那一类)。跨仓卡由 PM 转办。

一个随之而来的事实已另立卡(见 findings):spec 的 AriaPropsSchema.ariaLabelI18nLabelSchema,即也接受 inline locale map;本仓唯一的嵌套袋读点 ListView.tsx:2380as string 原样送进 DOM,map 值会念成 [object Object]。#8744 声明 aria 之后,本读点会继承同一空缺。

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

变异 预判 实测
M1 读点退回 aria?.label (a)(c) + 空串钉后半红,(b)(d)+ spec 契约钉绿 3 failed / 3 passed,红点落在 62 / 76 / 109 行 —— 109 正是空串钉的后半(ariaLabel:'' + label:'Legacy'),前半仍绿。符合
M2 塞假拼写 ariaLabelZ 同上 3 failed / 3 passed,同三处。证明钉确实沿 canonical 那一腿按名读取,幻影拼写喂不饱
M3 纯 ?? 链(代裁字面写法) 仅空串钉红,且落在前半 1 failed / 5 passed,红点落在 102 行(ariaLabel: '' 单独出现 ⇒ aria-label="")。量化了本 PR 对字面写法的偏离
M4 还原旧描述句 描述钉红,两条行为不动(仍绿) 1 failed / 2 passed。如实记录:行为钉在两个方向上都绿 —— 它们量的是 renderer,而改句子不改 renderer。这不是钉子失效,恰恰是「该回退从来不存在」的证据;句子本身只由文本钉守

变异后已还原,git diff 对 commit 为空,9/9 复绿。

验证

  • pnpm --filter '@object-ui/plugin-detail^...' build 通过
  • plugin-detail 全量:87 files / 824 tests passed
  • 仓根 pnpm type-check:81/81 successful
  • eslint(四个改动文件):0 errors(44 warnings 均为既有 no-explicit-any)
  • check:control-bytes OK(4562 tracked files);另对改动文件自扫 [\x00-\x08\x0b\x0c\x0e-\x1f] 无命中
  • changeset:.changeset/quick-actions-aria-label-and-actionnames-description-4663.md(patch)

半径严格限于 renderers/record-quick-actions.tsx + src/index.tsx 注册描述 + 两个测试 + changeset。未触及 #4646/#4647 席在飞的 RecordDetailView / RelatedList / list-toolbar 任何文件。


Generated by Claude Code

#4663)

同一组件的两处生产侧缺陷,objectstack#8744 测量该 renderer 读点时发现。

一、aria 读点双向落空。工具条只读 `schema.aria?.label` —— 恰是
`@objectstack/spec` 的 `AriaPropsSchema` 唯一拒绝的拼写:在那个封闭形上
`label` 是 alias 条目(指向 `ariaLabel` 的改名指示),存在意义是给出更好的
拒绝消息,从不被接受(实测:`safeParse({ label })` 返回 unrecognized_keys
并点名 `label`,`safeParse({ ariaLabel })` 通过)。于是 spec-valid 的
`aria: { ariaLabel: … }` 到达 renderer 后无人读取,内建 "Quick actions"
每次都赢;而 renderer 认的拼写,作者一写文档就被契约拒收。
`SchemaRenderer` 的通用 ARIA 通道也不是出口:它读扁平 `schema.ariaLabel`
并以组件 prop 注入 `aria-label`,而本 renderer 的 `splitDesigner` 连同其他
非 designer prop 一并丢弃。

读点改为 `(aria.ariaLabel ?? aria.label) || 'Quick actions'`:legacy 一腿
仅为契约封闭前的存量文档保留,两者同现时 canonical 赢。两半都照本仓既有的
同键惯例 —— `normalizeListViewSchema` 的 aria fold 仅在 canonical 为
`undefined` 时才搬运 legacy 键(故 `ariaLabel: ''` 遮蔽陈旧的 `label`),
而 `ListView` 自身读点把空串视为「没有可访问名」;因 `role="toolbar"` 必须
有名字,此处「没有名字」落到内建兜底,而非 ListView 的省略属性。

二、`actionNames` 注册描述承诺了不存在的回退(「else every action declared
for the object at this location」)。实测:无 `actionNames` 且宿主不传
`actions` 时 `namesToResolve` 为空、`needsLookup` 为 false、对象元数据从不
被查询、渲染虚线占位符。注册 `inputs` 是发布面(序列化进
`sdui.manifest.json` 与 JSX 授权类型,Studio 据此教作者),该承诺已流入工具
链 —— objectstack#8744 的派发提示逐字引用了它。按分诊口径改句子:实现该回退
属能力扩张,另立卡。回归测试驱动一个「有货」的元数据 provider,证明对象上已
声明的 action 确实不会被拉进来(并附控制用例证明同一套接线在有名字时照常
交付)。

Fixes #4663

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-BAxuLVBp.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.20KB 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.34KB 60.52KB
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) 31.55KB 10.70KB
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 分片,批次 23)

实物核验:merge-base 958d7573a;head 420c9cc30;5 files, +349/−3;标识 grep msg/diff 双零,trailer 唯一正确;releases/控制字节(含 ESC 位)零;#4646+#4647 席在飞文件零触碰(index.tsx 仅 quick_actions 块一行,PM 认领时已列入本单文件面)✅。CI 亲读:20/20 check 全 completed 零失败 ✅。

验收要点:

  1. 代裁字面被工程化修正且量化 —— 采信:(aria?.ariaLabel ?? aria?.label) || 'Quick actions' 而非纯 ?? 链:canonical-vs-legacy 用 ?? 对齐 normalizeListViewSchema 的 aria fold,内建兜底用 || 对齐 ListView 自身读点(空串 = 无可访问名)—— 纯 ?? 链会渲染出有 toolbar 角色却没有可访问名的元素,比兜底更差。这个偏离在代裁授权(「按同 bundle 既有惯例选并写明理由」)范围内,且被 M3 变异单独量化 —— 处置形完全正确。
  2. 前提三条实测硬度高:safeParse 双向(alias = 拒绝消息设施,固化成 envelope 断言测试 —— fix(plugin-grid): object-grid 的 data 输入按契约声明为 ViewData 对象 #5108 alias 实测形的复用)、resolveAriaProps 旁路确认断(嵌套袋唯一活路径)、fallback 用有货的元数据 provider 证伪(getItem 零调用 + 控制用例证明接线本身通)。
  3. M4 的如实记录(行为钉双向绿恰是「fallback 从来不存在」的证据,句子只由文本钉守)与描述钉的形制改道(installed spec 无 RecordQuickActionsProps 行可派生 ⇒ 行为式钉替代)—— 都是不套模板的正确判断。
  4. 新 finding finding: the spec's nested aria.ariaLabel admits an inline locale map, but its only read site passes it to the DOM raw — a map-valued accessible name announces "[object Object]" #5134(AriaPropsSchema.ariaLabel 是 I18nLabelSchema,inline map 值经 as string 上 DOM 念 [object Object])finding 入池正确,与 os#8744 声明 aria 后的继承空缺已点名。
  5. 跨仓 follow-up:os#8744 的「renderer 读 contract 拼写之日声明 aria」条件在本 PR 合入后满足 —— PM 将在 os#8744 留解锁评论(连同 finding: the spec's nested aria.ariaLabel admits an inline locale map, but its only read site passes it to the DOM raw — a map-valued accessible name announces "[object Object]" #5134 指名),转 objectstack 主队列跟进。
  6. 协作提示已记录:index.tsx 与 Related-list toolbar “+ New” does not consume userActions.create predicates — spec 17.0.0 shipped the declaration half, the renderer half is missing (createPredicates produced but never consumed) #4646 席可能行级冲突,该席收报时按 merge 冲突常规处置。

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


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 18, 2026 04:47
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit 7dd93c0 Aug 18, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4663-quick-actions-aria branch August 18, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants