Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

evalRowPredicate:宿主 scope 里的 record 键会盖掉行绑定 —— CEL 路盖得掉、遗留路盖不掉,同一函数两种优先级 #3796

Closed

Description

@yinlianghui

观察记录,实施 #3521 时发现,当前没有宿主会踩到(唯一的真实 provider 不注入 record),所以只作 finding 记录、不排期。

事实

packages/core/src/evaluator/listConditional.ts 的 evalRowPredicate 这样合并作用域:

const rowObj = toPredicateRecord(row && typeof row === 'object' ? row : {}, opts.fields);
const scope = { ...(opts.scope ?? {}), ...rowObj, data: rowObj };

data 被显式放在展开之后,所以宿主 scope 里的 data 键盖不掉行;record 没有同样的保护 —— 它靠两条求值路各自的引擎绑定:

  • 遗留路:new ExpressionEvaluator({ ...scope, record: rowObj }) —— record 在展开之后,行胜出。
  • CEL 路:evalFieldPredicate(pred, rowObj, fallback, undefined, scope) → 引擎收到 { record: rowObj, extra: scope }。实测 extra 压过 record 绑定:
ExpressionEngine.evaluate({ dialect: 'cel', source: 'record.x' },
  { record: { x: 'ROW' }, extra: { record: { x: 'SCOPE' } } })
// → 'SCOPE'

于是同一个 evalRowPredicate 调用,record.x 的含义取决于谓词是 CEL 还是遗留方言 —— 而方言路由是按字符串里有没有 === / ${…} 这类标记决定的,作者完全感知不到。

影响面(今天为零)

仓内唯一注入 predicate scope 的非测试宿主是 packages/app-shell/src/providers/ExpressionProvider.tsx,它给的键是 current_user / user / ctx / os / app / data / features —— 没有 record,所以现在没有任何面会撞上。测试里有人这么用过(PageBlockInspector.visibleWhen.test.tsx 传 scope={{ record }},但那条路不走 evalRowPredicate)。

会撞上的场合是将来某个宿主往 predicate scope 里放 record(看着很自然:ctx.record 已经有了),那时候行谓词会静默拿到宿主的 record 而不是这一行,并且只在 CEL 谓词上如此。所有走这条入口的面都会同时中招:行 kebab、批量选择栏、条件格式、看板卡片、以及 #3521 之后的 page:header。

可能的修法(留给分诊,不预设)

最小一致修法是给 record 与 data 同等的保护 —— 合并时把 record 也钉在展开之后(或在传给引擎前从 scope 里删掉 record),使两条方言路优先级一致:行永远胜过宿主 scope。行是这个函数的主语,宿主 scope 是背景,这个方向没有第二种读法;但它改的是 core 的绑定语义,值得一条测试把两条路的优先级一起钉住,而不是顺手改。

同时值得记一笔:page:header 现在(#3521 / PR #3791)会把整份宿主 scope 顶层绑定,与行 kebab 一致 —— 也就是说这条边缘从「只有行面有」变成「四个面都有」,这也是为什么单独记下来。

Activity

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Finding-triage ruling: Promote — finding to pm:queue.

    Concrete divergence with named lines and an uncontested one-line fix: evalRowPredicate's scope merge pins data but not record (listConditional.ts:168), the legacy path re-pins record after the spread (:175) while the CEL path passes scope as engine extras (:190/:192) — one function, two priority orders, and the edge spans four surfaces post-#3521. Fix plus pin test.

    Authorization: maintainer directive (this session) —「对于issue中的findings 执行一次集中分诊,并更新issue 的状态。」Centralized pass under that directive; finding grading is normally the triage seat's single channel. Round: objectstack#4949 finding-triage, 2026-08-10.
    Session: 01JaVVMrSxt7Tgi1uwEuDtH7


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    ContributorAuthor

    Claim: packed into #3792's dispatch (same file, one branch claude/issue-3792-3796-listconditional-diagnostics) — full claim comment on #3792. Session: session_017Qqyix2QcnpUC9XeYVDzx3.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions