feat(spec,lint)!: give bulkActionDefs a shape, and lint the aggregate name it references - #4508
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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对 widgetconfig的判断同源。help/helpText、default/defaultValue、object/reference,外加ActionParamSchema压根没有对应项的labelField)。objectui 已经为晋升方向准备了转换器(toBulkParam);收敛授权方向要教渲染器把授权参数也过一遍它,并给ActionParamSchema补labelField—— 那是一次带自己迁移的跨仓改动,不该搭在「给 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'且无内联actionDef。update/deletedef 的name是按钮 id,拿它去stack.actions里解析是无稽之谈。顺带修掉同一条规则里的两处:
listViews。对象没有顶层list,所以这一层一直没被访问过,而 view 层的早就覆盖了。locations—— 选中条是唯一不按位置过滤的表面(objectui#3142 / feat(lint): flag an action nobody placed — ADR-0078 Phase 3action-locations#4501 已把这点写进文档),在那里点名动作就是它的放置。配套
ui/新增bulk-action.zod.ts一行(3 sites)、section 总数 197 → 200,并说明为什么其中两个 site 是故意开着的。cel-bulk-action-visible一行。visible一直被求值(objectui#3067,逐条选中记录求值、fail-closed),但它藏在z.any()里,那道 ratchet 从来看不见它 —— 类型化之后才第一次被点名,这本身就是「一行里给出的、支持类型化的论据」。references/ui/bulk-action.mdx全部再生成;content/docs/ui/views.mdx修掉了「execution默认'perRecord'所以存量视图不受影响」这句现在已不成立的话。测试与验证
app-showcase真实的 5 个 def,含object/labelField这类写法 —— 给一个曾是z.any()的表面定型,只有在它治下的现存书写仍然合身时才是安全的)。packages/spec7096 项、packages/lint752 项、packages/cli694 项全通过;pnpm lint、pnpm typecheck(122 任务)、pnpm build、check: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