fix(predicate): 空谓词与非谓词值不再是门 —— 收敛最后三处自带范围的判定 (#3955, #3957, #3960) - #3977
Merged
yinlianghui merged 1 commit intoAug 9, 2026
Merged
Conversation
objectui#3850 把「这是不是一个已声明的门」下沉成 `@object-ui/core` 的唯一定义 `hasDeclaredPredicate`。它的落点条款漏下三处,缺陷同构:`evaluateCondition` 对 「没有东西可求值」的唯一默认是 `true`(意为 visible/enabled),只要一个过宽的 「已声明」判定把空谓词交给它、而键的极性是反的,那个 `true` 就会为一个元数据 从未表达过的值把控件关掉。 - **#3955 `SchemaRenderer` 的 `hidden` / `hiddenOn`**:问 `!== undefined` 且 verdict 不取反,于是空谓词 = HIDE,节点整个消失 —— 且该块在 `useMemo` 里不带 type 判定,覆盖走 SchemaRenderer 的全部节点。比 #3862 修掉的 `disabled` 孪生 更难诊断:置灰的控件还在界面上,没渲染的节点与「元数据本意隐藏」无法区分。 两条腿改读唯一定义。 - **#3960「空」判据扩到信封 `source`**:定义 trim 了裸串却没 trim 信封的 `source`(`toPredicateInput` 只折叠 `source === ''`,不 trim),于是 `{ dialect: 'cel', source: ' ' }` 是「已声明」的门,而它的 verdict 恰恰来自 core 自己的 CEL 入口把同一个值称作「没有谓词」(`if (!source.trim()) return true`)—— `disabled` 永久置灰,`ActionRunner.execute` 返回 `{ success: false, error: 'Action is disabled' }` 且 handler 从未被调用。 空白判据现在在定义处一次覆盖两种拼法。落点选定义处而非归一器:归一器答的是 「求值器接受什么形状」,与「有没有条件」是两个概念,把 trim 放进归一器会改到 每一个 `useCondition(toPredicateInput(…))` 调用点(含从不问这个问题的 action-bar / action-group / action-menu / RelatedList / record-alert 的容器级 `visible`),影响面远大于本单。 - **#3957 `ActionEngine.getActionsForLocation` 的 `visible` 过滤**:收敛后最后 一处自带范围的消费者 —— 手工折三种空拼法、其余用 `Boolean(raw)` 强转。改读 唯一定义,强转分支随之删除,同一个值不再因「经引擎浮现」还是「独立渲染」 得到两个答案(#3314 立的不变量)。谓词抛错时的 fail-CLOSED 姿态不动 (`throwOnError: true` + `warnHiddenPredicate`):「谓词求值失败」与「压根没有 谓词」是两个事实。 行为变更(观察类,无已知用户路径):引擎面 `visible: 0` / `NaN` / `' '` / `{ source: ' ' }` 从隐藏变显示;渲染面 `hidden: ''` / `null` / `' '` / 空信封 / 非谓词值从消失变渲染;`disabled: { dialect, source: ' ' }` 从置灰变可用、执行 入口从拒执行变执行。逐条写进 changeset 与钉。别名优先级有一处变化并按变更钉住: 未声明的 `hidden` 不再短路,声明了的 `hiddenOn` 终于被读到。 `SchemaRenderer` 的四条 `visible*` 腿按 #3850 裁决保持 `!== undefined` 不动 (取反后空谓词本就落在显示,收窄只改别名优先级)。 反向验证(先预判方向再跑):把三处分别改回旧范围,对应钉精确翻红 —— #3955 改回 `!== undefined` 红 19 项;#3960 去掉信封半边红 21 项;#3957 恢复 手工范围红 11 项;三处的真谓词 / `true` / `false` 用例全程绿。 Fixes #3955 Fixes #3957 Fixes #3960 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3955 + #3957 + #3960 三单结对落地 —— 空谓词收敛工程收官,仓内「已声明门」只剩一个定义、零自带范围的消费者。 核验记录(head
Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 9, 2026 19:54
yinlianghui
deleted the
claude/issue-3955-3957-3960-predicate-scope-converge
branch
August 9, 2026 19:55
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.
Fixes #3955
Fixes #3957
Fixes #3960
objectui#3850 把「这是不是一个已声明的门」下沉成
@object-ui/core的唯一定义hasDeclaredPredicate(PR #3966)。它的落点条款漏下三处,三处缺陷同构:evaluateCondition对「没有东西可求值」的唯一默认是true(意为visible/enabled),只要一个过宽的「已声明」判定把空谓词交给它、而键的极性是
反的,那个
true就会为一个元数据从未表达过的值把控件关掉。本 PR 把这三处一并收敛到同一个定义上,收敛完成后仓里没有第二处回答这个问题。
#3955 ——
SchemaRenderer的hidden/hiddenOn可见性链六条腿:四条
visible*写成!evaluateCondition(...),hidden/hiddenOn不取反。取反后那个true落在「显示」,与「没有门」同结果 ——良性;不取反就是 HIDE,于是
hidden: ''/null/ 纯空白 / 空信封让节点整个消失。该块在
evaluatedSchema的useMemo里不带 type 判定,覆盖走SchemaRenderer 的全部节点;而且比 #3862 修掉的
disabled孪生更难诊断 ——置灰的控件还在界面上,没渲染的节点与「元数据本意隐藏」在界面上无法区分。
两条腿改读唯一定义。别名优先级有一处变化,按行为变更钉住而不是当作等价:
未声明的
hidden不再短路,声明了的hiddenOn终于被读到。四条visible*腿按 #3850 裁决保持
!== undefined不动(取反后空谓词本就落在显示,收窄只改别名优先级)。
#3960 —— 「空」判据扩到信封的
source定义 trim 了裸串却没 trim 信封的
source(toPredicateInput只折叠source === '',不 trim),于是{ dialect: 'cel', source: ' ' }被判「已声明」,而它的 verdict 恰恰来自 core 自己的 CEL 入口把同一个值称作「没有谓词」
(
if (!source.trim()) return true)——disabled永久置灰,ActionRunner.execute返回{ success: false, error: 'Action is disabled' }且handler 从未被调用。空白判据现在在定义处一次覆盖两种拼法
(
isBlankPredicateText)。落点选择(定义处 vs 归一器),两个落点的影响面:
hasDeclaredVisibilityGatere-export 的五处 member-action 落点 +DeclaredActionsBar+record-quick-actions、SchemaRenderer的disabled/disabledOn(与本 PR 新增的hidden/hiddenOn)、ActionRunner两处执行门、
ActionEngine(本 PR 新增)。真正翻转的只有反极性键:disabled置灰→可用、执行门拒执行→执行、hidden消失→渲染。visible/enabled/condition上「已声明且求值为 true」与「未声明」同结果,不变。
if (!src.trim()) return undefined会让每一个evaluateCondition(toPredicateInput(x))消费者一起改判,包括从不问这个问题的容器级
visible读点(action-bar/action-group/action-menu/RelatedList/record-alert)—— 无 dialect 的空白信封{ source: ' ' }归一成
'${ }'求值为 falsy,在那些点上从隐藏变显示,属于没人裁过的面。更根本的理由:归一器答的是「求值器接受什么形状」,与「有没有条件」是两个概念
(refactor(types)!: Page/App/Dashboard 渲染节点不再占用 spec 的文档名 (#4115 B 组) #3074 / #4115 的分界),把 trim 放进归一器等于用形状问题回答声明性问题。
归一器契约因此未动,并在钉里正面断言(
toPredicateInput({dialect:'cel', source:' '})仍原样返回)。#3957 ——
ActionEngine.getActionsForLocation的visible过滤(A 案)收敛后最后一处自带范围的消费者:手工折三种空拼法,其余
Boolean(raw)强转。于是同一个值在引擎面与渲染面得到两个答案 ——
visible: 0/NaN引擎面隐藏、渲染面显示;
' '引擎面根本走不到强转分支(归一成'${ }',被求值为falsy)而隐藏,那一行分叉是 PR #3966 落地当天新产生的。改读唯一定义,
Boolean(raw)分支随之删除(声明了 ⇒ 归一后必有可求值的东西,undefined分支成为死代码)。谓词抛错时的 fail-CLOSED 姿态不动(
throwOnError: true+warnHiddenPredicate):「谓词求值失败」与「压根没有谓词」是两个事实。行为变更(观察类,无已知用户路径)
visibleSchemaRendererhiddendisabled(动作面 + 通用路径)disabled''/null' '(纯空白)0/NaN{}/[]{ dialect: 'cel', source: '' }{ dialect: 'cel', source: ' ' }{ source: ' ' }(无 dialect)true/false/ 裸 CEL /${…}/ 非空信封每一行都朝「这里没有门」移动,没有任何有 verdict 的值改变了 verdict:
声明了的
false仍是 verdict 而不是缺门(#3812),空白判据是trim()而不是「短」——
{ dialect: 'cel', source: ' x ' }仍是谓词。changeset 逐条写明。钉子与更新的 PR #3966 钉
新增/更新:
packages/core/src/evaluator/__tests__/declaredPredicate.test.ts——三行空白信封进 SHAPES 表;
documented residue (objectui#3960)用例 →收敛断言(并正面断言归一器契约未动);「三种空拼法」→「四种」;
DERIVATION 用例的具名分歧清单从 2 项扩到 5 项(与归一器的刻意差量现在是
「空白谓词文本的两种拼法」)。
packages/core/src/actions/__tests__/ActionRunner.disabledGate.test.ts——ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848 执行门新增两行空白信封(
blocked: false,parity 列同判),表头补上「本卡自己的修法之后仍被拦」的那两行与反向验证方向。
packages/core/src/actions/__tests__/ActionRunner.conditionGate.test.ts——DOCUMENTED DIVERGENCE(引擎面Boolean(raw)强转)→ 收敛断言(
toHaveLength(0)→['junk_visible'],并补visible: false仍隐藏的反变异钉);truthiness 表里那份内联再拼的
declared换成真定义的 import——定义已经下沉,留一份拷贝就是会漂移的孪生;新增空白信封行。
packages/core/src/actions/__tests__/ActionEngine.visibility.test.ts——新增 [finding] ActionEngine 的 visible 过滤仍用自己的「空/非谓词」范围:visible: 0 / NaN / ' ' 在引擎面隐藏、在渲染面显示 —— #3850 收敛后剩下的最后一处分叉 #3957 suite:13 行「没有门」逐行断言
hasDeclaredPredicate(v) === false且动作被浮现(把行集绑在定义上而不是绑在一份可能漂移的清单上)、「哪些行的 verdict 变了」行集断言、真谓词双向
反变异、抛错仍 fail-closed 且仍 warn。
packages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx(新)—— SchemaRenderer 的 hidden / hiddenOn 是 #3862 的反极性孪生:hidden: '' / null / 空 source 信封在通用渲染路径上让节点永久消失 #3955 全表:七种空拼法 ×
hidden/hiddenOn、junk 三行、true/false/表达式/CEL 信封双向、hidden键不作为 DOM prop 透传、两个方向的优先级 + 「删掉整条腿就翻红」的反删除钉、
visible*腿仍在最前且仍是
!== undefined。packages/react/src/__tests__/SchemaRenderer.disabledDeclaredGate.test.tsx—— PR fix(predicate): 「空谓词」不再算已声明门 —— 唯一定义下沉 core,渲染面 / SchemaRenderer / 执行门同读 (#3850, #3862) #3966 的
DOCUMENTED DIVERGENCE (objectui#3955)→ 收敛断言;EMPTY_SHAPES 补两行空白信封(「空谓词」的第四种拼法:{ dialect: 'cel', source: ' ' }(source 只有空白)仍被判成已声明门 → disabled 侧仍永久置灰、执行入口仍拒执行(#3850 裁决枚举未覆盖) #3960 在通用路径上的落点)。
packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx——引擎面/渲染面一致性钉([finding] ActionEngine 的 visible 过滤仍用自己的「空/非谓词」范围:visible: 0 / NaN / ' ' 在引擎面隐藏、在渲染面显示 —— #3850 收敛后剩下的最后一处分叉 #3957,消灭 ActionEngine.getActionsForLocation 把 CEL 信封塌回 legacy JS 求值路径,与每个渲染器的判决不一致 #3314 禁止的形状)。原有 suite 只比
verdict,而一个「面」是两个问题:渲染面是
hasDeclaredVisibilityGate(visible) && !isVisible。新增的rendererFace组合两问,17 行逐行断言
engine === renderer === 期望。反向验证(先预判方向,再跑;变异不提交)
三处分别改回旧范围,红的条数与分布与预判逐项一致:
hidden/hiddenOn改回!== undefinedTests 19 failed / 59 passed,分布逐项吻合;SchemaRenderer.expressions.test.tsx的真hiddenOn谓词全绿isBlankPredicateText的信封半边Tests 21 failed / 464 passed,分布逐项吻合Tests 11 failed / 387 passed,分布逐项吻合三次变异中
true/false/ 真谓词 / 非空信封 /visible*用例全程绿 ——这三处改动都只会「少关一个控件」,不会「多关一个」,方向单一。#3960 那次变异
同时就是 disabledGate 表头那两行「本卡修法之后仍被拦」的测量出处(变异复现的
正是
ab3ad4f3f上的范围)。验证
pnpm exec vitest run packages/core packages/react --maxWorkers=2——Test Files 109 passed (109)/Tests 2096 passed (2096)。pnpm exec vitest run packages/components/src/renderers/action packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx packages/plugin-detail/src/renderers/__tests__ --maxWorkers=2——
Test Files 16 passed (16)/Tests 263 passed (263)。packages/components源码未动(visibility-gate re-export 面不需要变)。
pnpm exec turbo run type-check --concurrency=2——Tasks: 78 successful, 78 total。node scripts/check-control-bytes.mjsOK;check-changeset-presence/check-changeset-no-major/check-changeset-fixed均 OK。🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Generated by Claude Code