Skip to content

refactor(app-shell): action-location 与目录查找两处手抄清单改为从 spec 派生 (#3017) - #3019

Merged
os-zhuang merged 1 commit into
mainfrom
claude/derive-spec-vocabularies-3017
Jul 30, 2026
Merged

refactor(app-shell): action-location 与目录查找两处手抄清单改为从 spec 派生 (#3017)#3019
os-zhuang merged 1 commit into
mainfrom
claude/derive-spec-vocabularies-3017

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#3017(源自 framework#3786 的模式排查)。metadata-admin inspector 里两处手抄 spec 词表,各配一句「保持同步」注释、零机制。两处目前都与 spec 一致 —— 这不是 bug 修复,是把「碰巧一致」换成「不可能不一致」。

对应 framework 侧:objectstack#4120(那边同一模式已查出四份真的漂移过的表单,全部静默失效)。

1. ActionDefaultInspector.LOCATIONS → 编译期保证

原先重述了 ACTION_LOCATIONS 的七个值。label 留在本地(那是 presentation),但词表改由 Record<ActionLocation, string> 承担 —— 让编译器当机制:

spec 侧变化 改前 改后
新增一个 location 下拉框里静默缺一项 TS2741: Property 'x' is missing
删除一个 location 下拉框里多一个死选项 TS2353: 'x' does not exist in type

两个方向都对着实际安装的 @objectstack/spec@17.0.0-rc.0 验证过(构造最小样例跑 tsc,确认 good 编译通过、missing/extra 各自报上表的错)。

展示顺序仍是对象的插入顺序,所以 record → list → global 这个便于作者理解的分组没变 —— 没有被 spec 的声明顺序带跑。

2. FlowReferenceField.KIND_TO_RECORD_LOOKUP → 运行期派生 + 测试闸门

原先为四个目录类 kind 硬编码了 object / valueField它自己的注释就写着「import it once a published release carries the export」—— @objectstack/spec@17 已经带上了 APPROVER_VALUE_SOURCES,所以现在真的 import 了,并把 objectui 的 presentation 组合在上面。

这张表是本模式已经让我们付过一次代价的地方:第一份数据契约抄错,把每个目录类 kind 都接到元数据注册表上,而那里根本不存放 sys_user/sys_team/sys_business_unit/sys_position —— 候选返回空、控件退化成自由文本框,于是 sales_manager 被打进只接受三个值的字段(framework#3508)。spec 当时的回应就是把 binding 发布出来,并用 satisfies 保证新增 ApproverType 不声明 binding 即编译错误。在这里读它,才是把那层保证跨仓传导过来。

spec 要求的分工被完整保留:spec 管候选从哪来、提交什么值;本包管 displayField / picker / subtitle。source 不是 data 的 kind 走跳过而非抛错 —— 模块级 throw 会因为一次词表变更把设计器整个白屏。

行为零变化

派生出来的表与它替换掉的字面量逐字段完全相同(已跑脚本比对)。recordLookupFor 的优先级不变:服务端发布的 binding 仍然优先,这张表仍是旧服务端的 fallback —— 只不过 fallback 的值现在来自编译进来的 spec,而不是一份手抄。

测试

新增 FlowReferenceField.specDerivation.test.ts(7 个用例),盯的是「派生有没有悄悄断掉」:表非空、四个 kind 都在、object/valueField 确实来自 spec、presentation 没在组合过程中丢失(尤其 user 的 people picker)。若 spec 把某个 kind 从 data 改走,测试会指名道姓地说「这个 picker 需要重新设计,不是重新派生一下就行」。

验证

  • pnpm --filter @object-ui/app-shell type-check —— 通过(先做了完整 workspace build,否则 @object-ui/* 解析不到会报一堆假错)
  • inspector 全量测试 33 files / 368 tests 全绿
  • lint 0 errors(包内 2054 条 warning 均为既有,我改的文件未新增)

未处理

#3017 里另外四处(object-fields-bridgeFieldDesignerRecordMetaFooterRecordDetailViewImportWizard 的两处 ↔ 服务端 import-coerce)本 PR 未动 —— 它们要么是 objectui 内部的两份清单(需要本地断言当闸门),要么需要 framework 先导出常量才能派生。留在 issue 里跟踪。


Generated by Claude Code

…vocabularies from the spec (#3017)

Two hand-copied spec vocabularies in the metadata-admin inspectors, each under
a comment asking the next author to keep it in step with `@objectstack/spec`
and nothing enforcing it. Both agreed with the spec today; neither had a
mechanism making that true tomorrow. objectui#3017 catalogues the pattern.

**`ActionDefaultInspector.LOCATIONS`** restated all seven `ACTION_LOCATIONS`
values. The labels stay here — they are presentation — but the vocabulary is
now `Record<ActionLocation, string>`, which makes the compiler the mechanism:
a location the spec ADDS is a missing-key error (TS2741) instead of a silently
absent dropdown entry, and one it REMOVES is an excess-property error (TS2353).
Both directions verified against the installed spec. Display order is the
object's insertion order, so the authoring-friendly grouping is unchanged.

**`FlowReferenceField.KIND_TO_RECORD_LOOKUP`** hard-coded `object` /
`valueField` for the four directory-backed kinds. Its own comment said to
"import it once a published release carries the export" — `@objectstack/spec@17`
does, so it now composes objectui's presentation on top of
`APPROVER_VALUE_SOURCES`.

That table is where this pattern has already cost us once: the FIRST copy of
the data contract wired every directory kind to the metadata registry, which
holds no `sys_user` / `sys_team` / `sys_business_unit` / `sys_position` ROWS,
so candidates came back empty, the picker degraded to free text, and
`sales_manager` got typed into a field that accepts three values
(framework#3508). Spec answered by publishing the binding behind a `satisfies`
that makes an undeclared `ApproverType` a compile error. Reading it here is
what carries that guarantee across the repo boundary.

The split the spec asks for is preserved: it owns WHERE candidates come from
and WHAT is committed; this package owns `displayField` / `picker` / `subtitle`.
A kind whose source is not `data` is skipped rather than thrown on — a
module-level throw would white-screen the designer over a vocabulary change.

No behaviour change: the derived table is byte-identical to the literals it
replaces. `FlowReferenceField.specDerivation.test.ts` pins that it stays wired
(non-vacuous, object/valueField sourced from the spec, presentation preserved).

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

vercel Bot commented Jul 30, 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 Jul 30, 2026 10:51am

Request Review

@github-actions github-actions Bot added the tests label Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
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.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
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) 457.75KB 99.96KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 134.67KB 34.24KB
fields (index.js) 222.07KB 54.35KB
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) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 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) 6.84KB 2.42KB
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.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.59KB 28.74KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 216.52KB 53.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 104.79KB 25.28KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 180.86KB 47.45KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 102.39KB 24.18KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.88KB 20.99KB
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) 3.47KB 1.54KB
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) 0.77KB 0.41KB
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 (index.js) 2.00KB 0.96KB
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.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 1.08KB 0.64KB
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

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 11:09
@os-zhuang
os-zhuang merged commit bf66b70 into main Jul 30, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/derive-spec-vocabularies-3017 branch July 30, 2026 11:10
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…ks, and curate six (#3027)

Seven record:* blocks shipped with renderers that read props but declared no
inputs. That combination is the worst of both: the renderer honours `limit`,
`severity`, `location` …, while every authoring surface — the designer panel,
the AI vocabulary, the generated manifest — reports the block takes no
configuration. #3013 recorded them as deliberately uncurated for exactly that
reason.

The declarations mirror what each renderer actually reads:

  record:activity        11  from RecordActivityComponentProps
  record:chatter         5   from RecordChatterComponentProps
  record:discussion      5   (same renderer, same inputs)
  record:alert           8   severity, title, body, visible, icon, action,
                             dismissible, dismissKey
  record:quick_actions   7   actionNames, requiredPermissions, location, align,
                             inline, variant, size
  record:history         3   limit, emptyText, unknownUserText
  record:reference_rail  1   hideEmpty

inputs describe what an AUTHOR writes, which is a subset of what the renderer
reads. `entries`, `loading` and resolved `actions` are injected by the host
shell off RecordContext; declaring them would invite a model to hand-write the
data the page is supposed to fetch. `aria` is omitted for the reason it is
omitted on record:details — an accessibility escape hatch, not a layout choice.
`location` takes its enum from the spec's ACTION_LOCATIONS rather than
restating it, per #3019.

Six of the seven join PUBLIC_BLOCKS: configurable and absent from the contract
is the state #3006 was about. The contract goes 36 -> 42 tags, all resolving.

record:chatter stays out — same renderer as record:discussion under a
Salesforce-familiar name, kept for schemas already in the wild. Two spellings
of one block is ambiguity an authoring model cannot resolve, so the vocabulary
carries the spec's name. A test compares the two input lists, so the day they
diverge the exclusion stops being justified and fails.

A companion assertion requires every curated record:* tag to declare inputs. A
curated tag with none reads as "takes no configuration" when the renderer in
fact reads props — the same gap #3006 opened, pointed the other way.


Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants