Skip to content

fix(app-shell): metadata-admin 谓词解析不到的路径 fail-open + dev 告警 (objectstack#6936) - #3937

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6936-predicate-fail-open
Aug 9, 2026
Merged

fix(app-shell): metadata-admin 谓词解析不到的路径 fail-open + dev 告警 (objectstack#6936)#3937
yinlianghui merged 1 commit into
mainfrom
claude/issue-os6936-predicate-fail-open

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixesobjectstack-ai/objectstack#6936

执行维护者裁决 C 案(objectstack#6936 评论 5230638168):metadata-admin 谓词求值器对「解析不到的路径」由静默判假改为 fail-open 判真 + dev 期告警

问题

packages/app-shell/src/views/metadata-admin/predicate.ts 的文件头一直承诺 fail-open(“on any parse error → returns true: better to show a field than to silently hide it”),但只有抛错走到那个 catch。标识符解析不到时走的是另一条路:resolveValue 逐段下钻取不到就返回 undefined,随后所有比较一律判假 —— ['text','number'].includes(undefined)undefined === 'text' —— 于是引用了不存在名字的谓词把字段静默隐藏。实现是 fail-CLOSED,与自述方向相反,且控制台一片安静。

踩法是版本错配窗口(objectui#3923 已浏览器实测):@objectstack/spec 17.0.0-rc.5 及更早把 objectForm 的子字段谓词写成裸拼法(type in ['text',…],共 16 条),而本引擎在 { data: draftRow } 作用域下求值。前端带着 objectstack#6331 的读取器修复、后端仍是旧版时,裸 type 解析为空,Studio 对象字段列表里 16 个类型相关子字段(Min / Max / Precision / Scale / Max Length / Min Length / reference / deleteBehavior / expression / returnType / autonumberFormat / language …)对所有行类型同时消失,用户看到的症状是「配置项没了」,与权限问题、类型不支持无法区分。

改动

  • 解析不到的路径 → 谓词判真,字段照常显示,兑现文件头一直写着的承诺。
  • dev 期告警,同时点名未解析的路径和承载它的谓词原文;按 (path, predicate) 对 warn-once,参照 @object-ui/corewarnOnUnknownActionKeys。memo 只按 path 记的话,只会报出 16 条里的第一条、对其余十五条保持沉默。

语义边界(本 PR 的承重部分)

「解析不到」= 路径的根标识符不是本作用域声明的名字typerecord.statuspage.selectedId,而作用域里唯一的名字是 data)。它等于「值算出来是 undefined」:

  • data.type == 'text' 在还没填 type 的草稿上 —— 根 data 解析成功,草稿只是那个键上没有值。这是合法的 undefined 值,照旧判假、字段照旧隐藏、不告警。草稿允许是空的;把 fail-open 扩大到「任何缺失值」会让一行新建字段一次点亮全部类型相关子字段(本 PR 在渲染层钉了这一条)。
  • data.tpye 这种深一段的拼写错误,在没有 schema 的情况下与「草稿没填」不可区分,而本求值器手上没有 schema。抓它属于生产端发布期校验谓词路径引用(伴生卡,已另立),不是渲染器可以猜的事(Commandment #0.1)。

失败信号用抛出内部错误而不是 sentinel 值,理由是 fail-open 是整条谓词的属性、不是失败那个子表达式的属性。sentinel 需要人工穿过 !&&||in== 逐个传递,第一个漏掉的运算符就会把结论反过来 —— !unresolvedPath 会把内层解析成「真」再取反回假,即换条路又回到 fail-CLOSED(这一条已单独钉住)。抛出则把所有失败都路由到本来就存在的那一个 fail-open 出口。

不变且已钉:原有 parse-error fail-open(仍静默 —— 路径解析成功、读取时炸了,是另一个事实);CEL 顺序的短路吸收(false && 不可解析 为假、true || 不可解析 为真,都不告警,因为那一半根本没被求值);CEL 风格的宽松 nullish 相等(空草稿上 data.type == null 仍为真)。

测试

新增 63 个断言,分两个文件:

  • predicate.test.ts(求值器层):①解析不到 → 真 + 告警内容含路径与谓词原文(含 ! 取反、错误作用域根、继承名 constructor、warn-once、同路径不同谓词各报一次);②解析到且真的不等 → 假、静默;③解析到但值为 undefined/null 的合法比较行为逐一钉住(含嵌套 data.config.kind== null 宽松相等);④原 parse-error fail-open 回归钉(getter 抛错 → 真且借用新告警);⑤短路吸收;⑥直接从安装版 spec 读 objectForm 的谓词做版本错配重现(16 条全裸拼法),外加一张字面量表把 16 条固化下来 —— spec 升到含 objectstack#6254 的版本后裸拼子集会合法地变空,所以「谓词总数大于 0」这条守卫无条件断言,避免整块变成对空集合的绿。
  • SchemaForm.unresolvedPredicate.test.tsx(渲染层,issue 里 B1/B2 两向读数的复现):B2 裸拼法下子字段重新出现且控制台有话说;B1 data. 拼法仍按行类型区分;未填类型的行仍保持子字段隐藏。

命令与结果:

pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ --maxWorkers=2
Test Files 133 passed (133)
Tests 1335 passed | 1 skipped (1336)
pnpm exec vitest run packages/app-shell --maxWorkers=2
Test Files 308 passed (308)
Tests 2835 passed | 1 skipped (2836)
pnpm exec turbo run type-check --concurrency=2
Tasks: 78 successful, 78 total

反向验证(方向先判后跑,预判为常规 Red):把 resolveValue 里新增的抛出那一段去掉后,63 条里 31 条转红(fail-open 组 9 条、垃圾表达式 1 条、可达的不可解析一半 1 条、bundled-spec 裸谓词 2 条、16 条字面量表、B2 渲染 2 条),典型失败是 AssertionError: expected false to be true;其余 32 条保持绿,正好是边界组(解析到判假、合法 undefined、parse-error 回归、短路吸收、B1 渲染)—— 这组保持绿正是它们钉的是改动前就有的取值语义、而不是新分支的证明。另外 objectstack#6331 留下的既有断言「both keys absent → shown」(data.type == 'list'{} 判假隐藏)在本改动下保持绿,如果边界放宽到「任何缺失值」它就会红。

changeset:@object-ui/app-shell patch。

⛔ 未折叠伴生卡(发布期校验谓词路径引用,producer 侧),⛔ 未碰 objectstack#5149 的运行时求值器文件,⛔ 未改 objectstack#6331 落地的 readVisibility 读取面。


Generated by Claude Code

…, loudly (objectstack#6936)
`predicate.ts` promised fail-open in its header and delivered it only for thrown
errors. A path whose ROOT identifier is not a name in the evaluation scope
resolved to `undefined`, and every comparison then judged it false —
`['text'].includes(undefined)`, `undefined === 'text'` — so a predicate
referencing a name that is not there HID the field, silently. Fail-closed, the
opposite of the documented promise.
Per the maintainer ruling on objectstack#6936 (option C): an unresolvable root
identifier now makes the predicate evaluate `true`, and dev mode warns once per
(path, predicate) pair naming both, mirroring `warnOnUnknownActionKeys`.
The boundary is deliberate and pinned: an absent draft VALUE (`data.type` on a
draft with no `type`) still resolves and still compares false — only an
unresolvable root fails open. The signal is a thrown internal error rather than a
sentinel so fail-open is a property of the whole predicate: a sentinel threaded
through `!` would negate back to false, i.e. fail-closed by another route.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@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 1:21pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DgOGDjj1.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.64KB106.39KB
core (index.js)3.00KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)231.13KB56.80KB
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)118.02KB30.47KB
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)109.67KB26.55KB
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)20.15KB6.72KB
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

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)objectstack-ai/objectstack#6936(维护者裁决 C 案)

实物核验:head 95f133a70,单提交 4 文件。CI 亲读终态:20 检查全 completed、0 失败。

验收要点:

  • C 案落地形态正确:解析失败以 UnresolvedPathError抛出、路由到既有 fail-open 门 —— 比 sentinel 穿线优越的论证成立(sentinel 过 ! 会取反回 fail-CLOSED,已用测试钉住 !unresolvable 为 true);warn-once 参照 warnOnUnknownActionKeys 先例,点名路径与谓词原文。
  • 语义边界画在根标识符且双向钉住:data.type 在空草稿上是合法 undefined 值照常判假(放宽会让新行全部条件子字段齐亮,且会打红 #6331 的既有断言);tpye 型一段深 typo 无 schema 不可分辨,归 producer 校验(伴生卡),渲染器不猜 —— 契约优先的正确取舍。
  • hasOwnProperty 防原型链名(constructor)误解析;CEL 短路吸收(false && 不可解析 → false 不告警)按 CEL 语义钉住。
  • 版本错配场景(rc.5 裸拼法 16 项)用安装版 spec + 版本无关字面表双重钉。
  • 反向验证 31/63 红且红绿分组与预判完全吻合(保持绿的恰是钉既有值语义的边界组)。

范围外 finding objectstack#7066(==/!= 右侧只走 parseLiteral,路径被当字面量比较 —— 今日休眠)已按规程立单持有,归下轮分诊。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 13:29
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit ebb579dAug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-os6936-predicate-fail-open branch August 9, 2026 13:30
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: metadata-admin 谓词求值器对「无法解析的路径」静默判假(fail-CLOSED),与文件自述的 fail-open 承诺相反

2 participants

@yinlianghui@claude