Skip to content

[finding] ActionEngine 的 visible 过滤仍用自己的「空/非谓词」范围:visible: 0 / NaN / ' ' 在引擎面隐藏、在渲染面显示 —— #3850 收敛后剩下的最后一处分叉 #3957

Description

@yinlianghui

来源

发现自 #3850 / #3862 结对实施(PR 分支 claude/issue-3850-3862-empty-predicate-gate)。#3850 把「已声明门」的唯一定义下沉到 @object-ui/core(hasDeclaredPredicate),渲染面(hasDeclaredVisibilityGate 保名 re-export)、SchemaRendererdisabled 链、ActionRunner 的两处执行门都改读它。收敛后只剩一处仍用自己的范围回答同一个问题:ActionEngine.getActionsForLocationvisible 过滤。

packages/core/src/actions/__tests__/ActionRunner.conditionGate.test.ts 里那条 DOCUMENTED DIVERGENCE 用例写的是「objectui#3850 的 follow-up 拥有统一它」—— #3850 落地时并没有统一(它的裁决面是「已声明门」的定义与放置,ActionEngine 的自有范围不在其中),所以这张卡把那个指针落成实体,而不是留一句指向不存在的 follow-up 的注释。观察类:需要作者写出非谓词值或纯空白谓词才命中,今天没有已知用户路径。严重度请分诊裁。

机理

packages/core/src/actions/ActionEngine.ts:229-243(origin/main @ 65bb513dc073deb4e72950c287eef32cb56bf8ce):

const raw = ra.action.visible;
if (raw == null || raw === '' || raw === true) return true;
if (raw === false) return false;
const expr = toPredicateInput(raw);
if (expr === undefined) {
  // Not an evaluable predicate: an envelope with an empty `source`
  // (→ nothing declared → visible), or a stray non-predicate value,
  // which keeps the historical `Boolean(raw)` coercion.
  return typeof raw === 'object' ? true : Boolean(raw);
}

三条腿各自处理了一部分「空」:raw === '' 显式放过、空 source 信封走 typeof raw === 'object'(与 #3850 裁决一致 → 显示)。剩下两类落到与共享定义不同的答案上:

  1. 非对象的非谓词值:Boolean(raw),于是 visible: 0 / NaN = 隐藏。共享定义答「不是谓词 → 没有门 → 显示」。
  2. 纯空白串:toPredicateInput(' ') 不折叠而是包成 '${ }'(≠ undefined),所以根本走不到上面那支,而是进入求值 → 求出 falsy → 隐藏。共享定义把纯空白明确列为「没有门」(它是 toPredicateInput 唯一不折叠、因而需要定义处点名的那一形状)。

一个值,两个入口,两个答案 —— 正是 #3314 立的那条不变量所禁止的形状。

实测(worktree @ 65bb513dc + 本 PR 的改动,一次性探针,未提交)

引擎面用 ActionEngine.registerAction + getActionsForLocation('record_section') 数结果条数;渲染面是 action:button 叶子渲染后 DOM 里有没有按钮。

value                     | ActionEngine  | action:button 渲染面(#3850 后)| 一致?
0                         | 隐藏          | 显示                          | 分叉
NaN                       | 隐藏          | 显示                          | 分叉
'   '(纯空白)            | 隐藏          | 显示                          | 分叉
{}                        | 显示          | 显示                          | 一致
''                        | 显示          | 显示                          | 一致
{dialect:'cel',source:''} | 显示          | 显示                          | 一致

其中 ' ' 一行是本 PR 造成的新分叉,如实记录:改动前渲染面对纯空白也是隐藏(与引擎面一致),#3850 的裁决把「纯空白 = 没有门」定为语义后,渲染面变为显示,而引擎面没有跟着改(不在裁决的改读清单里)。方向上渲染面是对的 —— 一个什么都没说的谓词不该把动作从所有人面前藏起来 —— 分叉在引擎面。

两条方向,以及为什么它需要裁而不是顺手改

更彻底的方向是 producer 侧:@objectstack/spec 在发布期就拒绝非谓词值与空谓词(跨仓,domain:spec),那样运行期两个入口都不必猜。#3850 的分诊已把同类的 producer 侧半边(空 source 信封的拒收)判为「无需求证据前不排」,本卡同理只作记录。

Related: #3850(「已声明门」范围裁决与定义下沉,本卡是它收敛后剩下的最后一处)、#3848 / #3872(ActionRunner 两处执行门的 fail-open 姿态出处)、#3314(一值一答的不变量)、#3862 / #3955(SchemaRenderer 侧的同族)。未认领。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions