Skip to content

feat(spec,lint)!: give bulkActionDefs a shape, and lint the aggregate name it references - #4508

Merged
os-zhuang merged 1 commit into
mainfrom
claude/bulk-action-aggregated-call-j95epo
Aug 1, 2026
Merged

feat(spec,lint)!: give bulkActionDefs a shape, and lint the aggregate name it references#4508
os-zhuang merged 1 commit into
mainfrom
claude/bulk-action-aggregated-call-j95epo

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4457

问题

选中条的一个批量动作此前声明为 z.array(z.record(z.string(), z.any())) —— 完全没有形状。真正的契约只活在 objectui 的 BulkActionDef 接口和读它的 executor 里,于是每一个书写错误都落成静默的运行期降级而不是解析错误:

  • opeartion: 'update' → 根本没有 operation → executor 的穷举 switch 落到 Unknown operation: undefined,每一行都来一次
  • excution: 'aggregate' → def 保持逐行模式,于是那个为一次 _selectedIds 调用而写的端点收到 N 次调用 —— 正是 objectui#3139 存在的意义所在的那个缺陷。
  • actionDef: {...} → 一个渲染器内部的 key(resolveBulkActions 解析名字时挂上去的),手写它看起来会生效,而 executor 确实会去派发里面的东西 —— 绕过动作注册表、绕过它的权限门、绕过它的参数契约。

这是 ADR-0018 的「第二套词汇表」气味(一个不共享 ActionSchema 任何检查的动作表面)叠加 ADR-0078 的 silently-inert metadata。

ui/bulk-action.zod.ts

ActionParamSchema#3746/#4001 拿到的同一套待遇:strict,未知 key 的报错点名冒犯的 key 并给出规范拼写(strictUnknownKeyError,含 actionDef / bulkEnabled / recordIdParam 三条专门的 guidance)。

除了拼写,它还拒绝 executor 根本不会读的组合:patch 不在 update 上、execution 不在 custom 上、params 落在 delete 上、batchSize 落在 aggregate 上。每一条在 #4457 之前都能解析,产出的是一个「报告成功却什么都没做的按钮」或一个被静默丢弃的 key。

一个此前能解析的形状现在被拒:operation: 'custom' 而不带 execution: 'aggregate'resolveBulkActions 只为一种授权形状挂 dispatcher(就是 aggregate 那种);其余 custom def 在 executor 里落到 Promise.resolve() —— N 个绿勾,零工作量。报错把两种合法写法都点名:逐行用 bulkActions: ['<name>'](晋升时带上动作自己的 label / params / visible),一次调用用 execution: 'aggregate'

两处刻意留开

  • params[].passthrough()。objectui 的 BulkActionParam 自己声明了 [key: string]: unknown —— 原样转发给字段渲染器的 widget 配置(min/max/step/format)。锁死它会拒掉合法配置,所以已声明的 key 类型化、其余透传,与 dashboard.zod.ts 对 widget config 的判断同源。
  • bulk param 与 action param 的拼写分歧(help/helpTextdefault/defaultValueobject/reference,外加 ActionParamSchema 压根没有对应项的 labelField)。objectui 已经为晋升方向准备了转换器(toBulkParam);收敛授权方向要教渲染器把授权参数也过一遍它,并给 ActionParamSchemalabelField —— 那是一次带自己迁移的跨仓改动,不该搭在「给 def 定型」这班车上。照它现在的样子类型化,正是让这条分歧可见而不是靠意会,也是关掉它的前置条件。

label 与参数/选项的 label 用 z.string() 而非 I18nLabelSchema:授权 def 是原样送到网格的(app-shell/ObjectView.tsx 直通,resolveBulkActions 明说授权 def「left as-authored」),这条路径上没有任何东西解析 { en, zh },而选中条把 def.label 当 React child 渲染 —— 祝福 map 形态等于把一个解析错误换成一次白屏。要本地化就把它声明成真正的动作再在 bulkActions 里点名,那条路径才过 i18n 解析器。

Lint

validate-action-name-refs 现在覆盖 bulkActionDefs,但只覆盖真的是引用的那一类:execution: 'aggregate' 且无内联 actionDefupdate/delete def 的 name 是按钮 id,拿它去 stack.actions 里解析是无稽之谈。

顺带修掉同一条规则里的两处:

配套

  • strictness ledger:ui/ 新增 bulk-action.zod.ts 一行(3 sites)、section 总数 197 → 200,并说明为什么其中两个 site 是故意开着的。
  • expression-conformance ledger:新增 cel-bulk-action-visible 一行。visible 一直被求值(objectui#3067,逐条选中记录求值、fail-closed),但它藏在 z.any() 里,那道 ratchet 从来看不见它 —— 类型化之后才第一次被点名,这本身就是「一行里给出的、支持类型化的论据」。
  • liveness note、authorable-surface(+26 key)、json-schema、api-surface、references/ui/bulk-action.mdx 全部再生成;content/docs/ui/views.mdx 修掉了「execution 默认 'perRecord' 所以存量视图不受影响」这句现在已不成立的话。

测试与验证

  • 新增 spec 18 项(规格取自 app-showcase 真实的 5 个 def,含 object/labelField 这类写法 —— 给一个曾是 z.any() 的表面定型,只有在它治下的现存书写仍然合身时才是安全的)。
  • lint 该规则 13 → 21 项。
  • packages/spec 7096 项、packages/lint 752 项、packages/cli 694 项全通过;pnpm lintpnpm typecheck(122 任务)、pnpm buildcheck:generated / check:liveness / check:strictness-ledger / check:spec-changes / check:exported-any 全绿。
  • 零误报实测:app-showcase / app-crm / app-todo 三个应用 objectstack lint 无新增发现,showcase 现有的 aggregate def 与 4 个 mass-update def 在严格化后照常解析、照常构建。

🤖 Generated with Claude Code

https://claude.ai/code/session_01S9aiswZBzoVYsyLKRuGByE


Generated by Claude Code

…te name it references (#4457)

A selection-bar bulk action was `z.array(z.record(z.string(), z.any()))` — no
shape at all. The real contract lived in objectui's `BulkActionDef` interface
and in the executor that reads it, so every authoring mistake landed as a silent
runtime downgrade: `opeartion` parsed and the executor hit `Unknown operation:
undefined` per row; `excution: 'aggregate'` parsed and left the def per-record,
so the endpoint written for ONE `_selectedIds` call got N calls instead.

`ui/bulk-action.zod.ts` types it with the treatment `ActionParamSchema` got in
#3746/#4001: a strict def whose unknown-key error names the offending key and
the canonical spelling. It also refuses the combinations the executor never
reads (`patch` outside an update, `execution` outside a custom, `params` on a
delete, `batchSize` on an aggregate) and a hand-written `actionDef`, which the
renderer attaches and which authored by hand would smuggle an action definition
past the action registry.

One shape that parsed before is now rejected: `operation: 'custom'` without
`execution: 'aggregate'`. `resolveBulkActions` attaches a dispatcher for exactly
one authored shape; every other custom def falls to `Promise.resolve()` per row
— a button that reports success for every selected record and does nothing. The
error names both legal forms.

`params[]` stays `.passthrough()` (the renderer declares a widget-config
catch-all), and the bulk-param/action-param spelling divergence is documented
rather than converged — that needs a cross-repo change, and typing them as they
are is what makes the divergence visible.

Lint: `validate-action-name-refs` now covers `bulkActionDefs`, for the entries
that are references rather than button ids (`execution: 'aggregate'`). The walk
also reaches an object's own `listViews` for the first time, and the hint no
longer tells a bulk-surface author to add a `locations` entry the selection bar
does not read.

Verified zero new findings against app-showcase / app-crm / app-todo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S9aiswZBzoVYsyLKRuGByE
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 1, 2026 1:59pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/l and removed documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/dogfood, @objectstack/spec.

108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, packages/qa/dogfood, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa/dogfood)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 14:19
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 97faca3 Aug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/bulk-action-aggregated-call-j95epo branch August 1, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[spec] bulkActionDefs is z.record(z.any()) — type the def shape (ADR-0018 second-vocabulary smell) and cover it in reference-integrity lint

2 participants