Skip to content

feat(core): declare the 18 spec-owned action keys ActionDef absorbed silently (objectstack#4075 第 2 步) - #3190

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4075-actiondef-step2-explicit-keys
Aug 2, 2026
Merged

feat(core): declare the 18 spec-owned action keys ActionDef absorbed silently (objectstack#4075 第 2 步)#3190
os-zhuang merged 1 commit into
mainfrom
claude/issue-4075-actiondef-step2-explicit-keys

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Refs objectstack-ai/objectstack#4075(第 2 步,不关闭)

分阶段路线的第 2 步。第 1 步(objectui#3032)已经把「未识别键」变成 dev 警告并盘点出清单;本 PR 把清单里合法的那一半变成真正的字段。第 3 步(删除索引签名)仍未做,issue 保持打开。

背景

ActionDef[key: string]: any 结尾,因此任何键、任何类型都能通过编译。错拼(targt)和已退役的 spec 键(execute)都能 type-check,然后 runner 静默地不绑定任何 handler —— 这正是 #2169「Mark Done 什么都不做」的形状。

本 PR 做了什么

1. 18 个 spec 拥有、ActionDef 从未声明的键,提升为显式可选字段

aiariabodyExtrabodyShapebulkEnabledcomponenticonlocationsmodeobjectNameorderrecordIdFieldrecordIdParamrequiredPermissionsrequiresFeatureshortcutvariantvisible

每个类型都从 @objectstack/specActionInput 派生(SpecActionInput['locations'] 等),不手抄。手抄一份 spec 形状等于造出第二份会静默漂移的契约 —— 而这正是本 issue 要解决的失败模式本身。效果:order: 'first'variant: 'chartreuse'locations: ['nope'] 现在是编译错误,此前被索引签名静默吞掉。

2. 从 z.input 派生,而不是 z.infer —— 这一点是承重的,不是风格选择

ActionSchema 是一个 ZodPipe,它的 transform 会收窄作者写下的形状:visible 的作者侧是 string | { dialect, source },但 infer 之后只剩下 envelope 对象那一支。本 runner 消费的是未经解析回灌的 authored/stored 行(#3903 已确立),看到的是 input 形状。若按既有 ActionComponent 的先例从 Action(z.infer)派生,就会把 ActionEngine 明确支持、且被 ActionEngine.visibility.test.ts 钉住的裸字符串谓词判成类型错误。component 是纯 enum,两者恰好重合,所以旧先例没有暴露这个差异。

3. 删掉 ActionEngine 里 3 处 as any

visiblerequiredPermissions(位置过滤器)、locations(注册)。这些 cast 只因字段未声明而存在。该文件 as any 从 9 处降到 3 处,剩下的 3 处与本 issue 无关(ActionContext 的开放数据袋、evaluator API),按 issue 要求不动 ActionContext 的索引签名

4. 只给 4 个 objectui 方言键标 @deprecated

actionTypetype;apiendpointtarget(executeAPI 本就按 api || endpoint || target 解析);navigate → 扁平的 target / openIn(executeNavigation 本就回退到它们)。

刻意没有给其余 13 个方言键加 @deprecated:它们是 runner 自身的机制(chain/chainMode、onSuccess/onFailure、toast、close、reload、redirect、modal、confirm、condition 等),spec 里没有对应拼写,指向一个不存在的替代品比保持现状更糟。

说明一处与派发指令的偏差:派发要求把 to/external/newTab/replace 标记为 deprecated,但第 1 步的盘点结论恰恰相反 —— 它们是 navigation 别名自己的合法拼写,已被显式声明并配有「spec 若采纳其中之一即失败」的 tripwire 测试。给它们标 deprecated 会与第 1 步的结论直接冲突,故未执行。

一个刻意的破坏性边缘

shortcutbulkEnabled 派生出来是 undefined 而不是可用类型 —— 这是正确结果。spec 17 把两者退役为 retiredKey() 墓碑(z.never()),作者写任何一个都是硬解析拒绝。派生(而非手写 shortcut?: string)零成本地把这个运行时拒绝提升成了编译错误;手抄则会悄悄把两个死键重新合法化。

因此:此前能编译的 shortcut: 'ctrl+k' 现在编译不过。这类元数据本就已被平台拒绝,本 PR 只是把失败挪到能被修复的地方。宿主仍可通过 ActionEngine.registerAction(action, { shortcut, bulkEnabled }) 显式传入。

changeset 因此标 minor 而非 patch —— AGENTS.md 的版本策略明确要求 objectui 自身的破坏性变更标 minor(绝不 major),正文里写清 breaking 语义。派发指令写的是 patch,这里按仓库约定取 minor;若维护者认为该边缘不算 breaking,改一个词即可。

测试

新增 packages/core/src/actions/__tests__/actionKeys.types.test.ts(30 条)。被验证的性质是「编译器会拒绝错误类型的值」,运行时断言观察不到,而 expectTypeOf 在本仓不可用(vitest.config.mts 未开 typecheck 模式,开启它属于 objectui#3181 的门禁改动,不该搭本 PR 的车)。所以测试用 TS 编译器 API 把一小段虚拟模块编译到真实的 ActionDef 上,再读回诊断。

区分度是内建的,不是靠正文声称的:同一批坏赋值会被第二次编译到 IndexSignatureOnly(一个只有 [key: string]: any 的接口,即第 2 步之前的 ActionDef),并要求全部干净通过。这个对照组就是「revert 即红」的证明:撤掉提升,索引签名重新把它们吞掉,断言即由绿转红。

已实测:把 order?: SpecActionInput['order'] 改成 order?: any,精确地红 2 条(order rejects a string where the spec says number 与区分度断言)。

按 AGENTS.md 测试纪律,编译在模块作用域完成(约 2.9s 落在 import 阶段,不受任何 test/hook 超时约束),而不是放进 beforeAll(那会落到更窄的 10s hookTimeout 下,是把慢 setup 变成负载相关 flake 的标准做法)。

  • vitest run --project unit:330 files / 4619 passed
  • vitest run --project dom:433 files / 4466 passed
  • pnpm type-check(全仓):0 errors
  • pnpm --filter @object-ui/core lint:0 errors;改动的三个文件中 actionKeys.ts 与新测试 0 warning,ActionEngine.ts 的 warning 数因删 cast 而下降

不在本 PR 范围

索引签名保留 —— 删除它是第 3 步。断言「索引签名仍然存在」的那条反向 pin 保持绿色,它是本 issue 自己的完工检查。


🤖 Generated with Claude Code


Generated by Claude Code

…silently

`ActionDef` ends with `[key: string]: any`, so it accepted any key of any
type — a typo (`targt`) and a retired spec key (`execute`) both type-checked,
then the runner silently bound no handler (the #2169 shape). Step 1 made that
audible with a dev-mode warning; this is step 2, promoting the keys that
warning identified as legitimate into real fields.

- Promote 18 spec-owned keys to explicit optional fields, every type DERIVED
  from `@objectstack/spec`'s `ActionInput` rather than hand-copied. A
  hand-written duplicate of a spec shape is a second contract that drifts,
  which is the failure this issue is about.
- Derive from `z.input`, not `z.infer`: `ActionSchema` is a `ZodPipe` whose
  transform narrows `visible` to the `{ dialect, source }` envelope alone.
  This runner reads authored rows unparsed, so it sees the input shape;
  `Action` would have rejected the raw-string predicate ActionEngine supports.
- Delete three `as any` casts in `ActionEngine` (`visible`,
  `requiredPermissions`, `locations`) that existed only because the fields
  were undeclared. Package `as any` count drops 9 -> 3.
- Mark `@deprecated` only the four dialect keys the runner itself proves are
  aliases: `actionType` -> `type`, `api`/`endpoint` -> `target`, `navigate` ->
  flat `target`/`openIn`. The rest are runner mechanics with no spec spelling.

`shortcut` and `bulkEnabled` derive to `undefined` because spec 17 retired
both as `retiredKey()` tombstones — so authoring either, already a hard parse
rejection, is now a compile error too. Hand-copying would have quietly
re-legitimized two dead keys.

The index signature stays; removing it is step 3, and the inverted pin
asserting it is still present remains the issue's completion check.

Refs objectstack-ai/objectstack#4075

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

vercel Bot commented Aug 2, 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)
objectui Ignored Ignored Aug 2, 2026 9:48am

Request Review

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 09:50
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit bca45cc Aug 2, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4075-actiondef-step2-explicit-keys branch August 2, 2026 09:50
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
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) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 475.95KB 104.41KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.47KB 10.67KB
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.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
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.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.53KB 17.12KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.87KB 28.82KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 224.52KB 54.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 184.69KB 48.91KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.87KB 25.31KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.48KB 10.57KB
plugin-timeline (index.js) 25.76KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
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 (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) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants