Skip to content

fix(app-shell): SchemaForm 读取归一化后的 visibleWhen,让 metadata-form 谓词首次真正生效 - #3923

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6331-schemaform-visiblewhen
Aug 9, 2026
Merged

fix(app-shell): SchemaForm 读取归一化后的 visibleWhen,让 metadata-form 谓词首次真正生效#3923
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6331-schemaform-visiblewhen

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#6331

问题

ADR-0089 把 FormView 的可见性谓词从 visibleOn 改名为 visibleWhen,spec 的归一化是改写而非双写:parse 之后的 FormView 只带 visibleWhen,visibleOn 完全不存在。而 metadata-admin 的 SchemaForm.tsx五个谓词读取站点只读 visibleOn,于是每一条 spec 下发的谓词都读到 undefined,guard 短路成「可见」—— 所有 metadata 表单的条件字段、条件分区、条件页签一律恒显。

一次性探针跑在本仓实际安装的 @objectstack/spec@17.0.0-rc.5 上(读数入 issue 报告):

objectForm 70 sub-fields / 16 predicates / ANY_visibleWhen true / ANY_visibleOn false
pageForm 4 predicate nodes,全部 visibleWhen
viewForm 7 predicate nodes,全部 visibleWhen
actionForm 6 predicate nodes,全部 visibleWhen

33 条谓词,一条都没生效过。

修法

五个站点统一走一个 readVisibility()(canonical 在前):扁平逐属性、分区级、分区内字段级、tabbed 路径的字段探测、type: 'record' 行内子字段。拼法与优先级镜像仓内既有适配,不新造方言 —— @object-ui/plugin-formsectionFields.ts@object-ui/reactform-view.ts 早就是 visibleWhen ?? visibleOnFormSectionSpec / FormFieldSpec 两处类型同时声明两个键,别名标 @deprecated

别名这条腿保留不是「容错」,而是它有活的生产者:本 app 自己的 create schema 直接把 visibleOn 写在原始 JSONSchema 属性上(view-create-body.tsanchors.ts),这条路径根本不经过 spec 归一化。

行为变更面(这些谓词从未在发布版里生效过,本 PR 是第一次打开)

  • Studio 对象字段列表按类型显隐行内子字段:currency 字段只出 Min / Max / Precision / Scale,text 字段只出 Max Length / Min Length,不再一次性全出。
  • Page 编辑:list 页隐藏 Data Context / Layout / Template 并显示 Interface 分区,record 页反之。View / Action / Report 表单同样拿回各自的类型条件分区与字段。

浏览器实证

临时探针页(未提交)把真实 bundled spec 表单喂进真实 SchemaForm,Playwright 驱动 Chromium 读数:

  • pageForm,type: home → 分区 Basics / Data Context / Advanced,字段含 Template;切到 type: list → Data Context 与 Template 消失,Interface (list pages) 整棵子树出现。
  • objectFormfields record 节点(#6254 后的 data. 拼法),行 type: currencyMin, Max, Precision, Scale;切到 textMax Length, Min Length。两向都对。

反向验证(方向先判后跑,逐站点点名)

五个站点各自还原成只读 visibleOn,红点与预判完全一致:站点1 → 扁平 2 红;站点2 → 分区 2 红 + bundled pageForm 渲染 1 红;站点3 → 字段级 3 红;站点4 → tabbed 2 红;站点5 → record 行 2 红。

一处预判需要更正:我原本写「别名 describe 在任何还原下都保持绿」,但站点2 还原时多出第 4 个红点 —— 那条「canonical wins」测试虽然写在别名 describe 里,实际钉的是 canonical 优先级(删掉 canonical 腿后由过期别名定胜负,必然红)。已按它真正守的东西把它移进独立 describe 并注明。别名测试本身在每次还原下确实全绿 —— 这正是这个 bug 能长期潜伏的原因:别名拼法的 fixture 永远看不见它。

rollout 注意

谓词必须是 data. 作用域才能对着草稿求值(objectstack#6254 修正了 object.form.ts 里 16 处裸拼法)。仍在下发 #6254 之前裸拼法的后端,配上本 PR 会得到相反症状 —— 这些子字段变成恒隐而非恒显,因为本引擎的求值器把未作用域标识符解析成 undefined、谓词判假。浏览器探针里两种拼法并排验证过。求值器这个「未解析标识符静默判假、与自身文档承诺的 fail-open 相反」的行为已另立 finding 单,不在本 PR 范围。

验证

  • pnpm --filter '@object-ui/app-shell^...' build
  • SchemaForm.visibleWhen.test.tsx + 既有三个 SchemaForm 测试文件 + repeaterUnion:Test Files 5 passed (5) / Tests 26 passed (26)
  • 消费半径全量:packages/app-shell/src/views/metadata-adminTest Files 131 passed (131) / Tests 1272 passed | 1 skipped
  • packages/core 的 column-identity ratchet(按文件计数的闸门,确认新增 ?? 不撞它):7 passed
  • 全仓 pnpm exec turbo run type-check --concurrency=2:78 successful, 78 total
  • node scripts/check-control-bytes.mjs:OK(3799 tracked text files)

Generated by Claude Code

…g every metadata-form predicate (objectstack#6331)
ADR-0089 renamed the FormView visibility predicate `visibleOn` -> `visibleWhen`,
and the spec normaliser REWRITES the alias instead of keeping both: a parsed
FormView carries `visibleWhen` and no `visibleOn` at all. All five predicate read
sites in the metadata-admin SchemaForm looked at `visibleOn` only, so every
spec-served predicate read as absent and each guard short-circuited to
"visible" - conditional fields, sections and tabs rendered unconditionally in
every metadata form.
Measured over the bundled @objectstack/spec@17: objectForm carries 16 sub-field
predicates, viewForm 7, actionForm 6, pageForm 4 - all spelled `visibleWhen`,
none spelled `visibleOn`. Every one was inert.
Read sites fixed (all via one `readVisibility()` reader, canonical first):
flat per-property, section-level, section field-level, the tabbed path's field
probe, and `type: 'record'` row sub-fields. Spelling and precedence mirror the
existing adapters - plugin-form `sectionFields.ts` and react `form-view.ts`
both already read `visibleWhen ?? visibleOn` - so the repo keeps one dialect.
`FormSectionSpec` / `FormFieldSpec` now declare both keys, alias `@deprecated`.
The alias stays honoured because it has live producers that never pass through
the spec normaliser: this app's own create schemas set `visibleOn` directly on
raw JSONSchema properties (`view-create-body.ts`, `anchors.ts`).
Tests: SchemaForm.visibleWhen.test.tsx pins each of the five sites in both
directions, the alias limb, canonical-over-alias precedence, and the real
bundled `pageForm` (so a regression cannot hide behind a hand-transcribed
fixture). `SchemaForm.pageVisibility.test.tsx`'s doc comment is corrected: its
`visibleOn` fixtures pin the deprecated limb, not the shape a backend serves -
which is why they stayed green while every real predicate was dead.
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 4:55am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-CBjvHWPr.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.39KB106.34KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)230.97KB56.74KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)117.21KB30.27KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

正文里提到的求值器 finding 已立单:objectstack-ai/objectstack#6936(未认领、未进队列,交 PM 分诊)。

要点:metadata-admin/predicate.ts 的文件头承诺「任何解析错误 → 判真(fail-open),宁可显示也不静默隐藏」,但「标识符路径解析不到」不走抛错分支 —— resolveValue 返回 undefined,includes(undefined) / undefined === 'x' 都判假,于是静默隐藏,与自述方向相反。本 PR 修好读取器之后这条语义才第一次可达,所以它是本 PR 的下游事实、不是本 PR 引入的问题;修改求值语义是独立取舍(判真 / 判真+告警 / 保持判假),不夹带进读取器修复。


Generated by Claude Code

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#6331(v17)

实物核验:base 99ba5fbd7(origin/main)→ head 5c84cad58,单提交 4 文件;五个读取站点(扁平 :575 / 分区级 / 分区字段级 / tabbed 探测 / record 行内)全部收敛到 readVisibility(),canonical visibleWhen 在前,拼法镜像 plugin-form/sectionFields.ts@object-ui/react form-view.ts —— 仓内单一方言成立,不是新方言。别名腿保留有据(view-create-body.ts/anchors.ts 直写原始 JSONSchema,不经 spec 归一化)。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • spec 侧探针数据扎实:安装版 spec@17 里 objectForm 16 / viewForm 7 / actionForm 6 / pageForm 4 条谓词全拼 visibleWhen、零 visibleOn —— 33 条谓词此前全部失效,本 PR 是首次生效,changeset 如实declared行为开启面。
  • 浏览器两向实证(pageForm type 切换、objectForm currency/text 子字段)可信。
  • 反向验证五站点逐一还原各自转红;「canonical wins」测试被移进独立 describe 的更正合理(它钉的是优先级,不是别名腿)。
  • SchemaForm.pageVisibility.test.tsx 的文档注释改为如实说明其钉的是废弃别名腿 —— 正是 bug 长期潜伏的机制,这个说明有档案价值。

范围外发现 objectstack#6936(求值器对解析失败静默判假,与 fail-open 自述相反;前后端版本错配时 16 个配置项静默消失)已按规程立单未认领,归后续分诊 —— v17 冻结期不派。

GraphQL 配额未恢复则 undraft+auto-merge 由 PM 随后补发。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 05:38
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit 7a197e7Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os6331-schemaform-visiblewhen branch August 9, 2026 05:39
Sign up for freeto 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.

objectui: SchemaForm 五个站点只读 visibleOn,而 ADR-0089 归一化输出只含 visibleWhen —— 全部 metadata-form 谓词死路,字段恒显

2 participants

@yinlianghui@claude