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.

「空谓词」的第四种拼法:{ dialect: 'cel', source: ' ' }(source 只有空白)仍被判成已声明门 → disabled 侧仍永久置灰、执行入口仍拒执行(#3850 裁决枚举未覆盖) #3960

Closed

Description

@yinlianghui

来源

发现自 #3850 / #3862 的结对实施(PR 分支 claude/issue-3850-3862-empty-predicate-gate)。给下沉后的唯一定义 hasDeclaredPredicate 写单元钉时,我先假设「空白 source 的信封也会在归一器折成 undefined」,这条断言红了 —— 一路量下去发现它是同族里第四种「空」拼法,而 #3850 的裁决只枚举了三种('' / 纯空白串 / { dialect, source: '' }),所以它在那个 PR 之后依然是坏的,症状与 #3850 / #3862 一字不差。

按「范围外发现只立卡不顺手修」的纪律,本卡未认领、不在那个 PR 里修:裁决刚刚就「哪些拼法算空」做过一次显式枚举,把第四种加进共享定义会同时改到 #3848 已合入并钉住的执行门行为,那是 PM 该裁的一步,不是实施者顺手定的。严重度请分诊裁。

机理

toPredicateInput(packages/core/src/evaluator/predicateInput.ts)对信封只判 if (!src) return undefined —— 不 trim:

if (typeof value === 'object' && typeof (value as { source?: unknown }).source === 'string') {
  const src = (value as { source: string }).source;
  if (!src) return undefined;                       // '' 折叠,'   ' 不折叠
  if ((value as { dialect?: unknown }).dialect === 'cel') return { dialect: 'cel', source: src };
  return wrapIfBare(src);
}

于是 { dialect: 'cel', source: ' ' } 原样归一成一个 cel 信封,hasDeclaredPredicate 判「已声明」,而求值侧 ExpressionEvaluator.evaluateCelCondition 第一行就是

if (!source.trim()) return true; // no predicate → visible/enabled

—— 也就是说 core 自己的求值器把空白 source 明确称作「没有谓词」(evalRowPredicate 在 evaluator/listConditional.ts 用同一条 !source.trim() 规则),唯独「有没有声明门」这一问答了「有」。夹缝重新出现:门说已声明、求值说没有条件(→ true),在 disabled 上就是永久置灰。这正是 #3850 正文里那张表的机理,只是空白从裸串搬进了信封。

共享定义 trim 了字符串拼法(typeof value === 'string' && value.trim() === '')却没 trim 信封的 source,这处不对称是本卡的全部内容。

实测(worktree @ 65bb513dc + #3850/#3862 的 PR 已应用,一次性探针,未提交)

value                        | declared | 归一后               | evaluateCondition | action:button disabled | action:button visible | SchemaRenderer disabled prop | ActionRunner.execute
{dialect:'cel',source:'   '} | true     | {dialect:cel,src:'   '} | true            | 置灰                   | 显示                  | true(置灰)                  | {"success":false,"error":"Action is disabled"},handler 未跑
{source:'   '}(无 dialect)  | true     | "${   }"             | true              | 可点(良性)            | 隐藏                  | true(置灰)                  | 同上,拒执行
{dialect:'cel',source:'\n'}  | true     | {dialect:cel,src:'\n'} | true            | 置灰                   | 显示                  | true(置灰)                  | 同上,拒执行

三个入口(动作面渲染器 / SchemaRenderer 通用路径 / 执行入口)在这一形状上全部落在「有门且为真」:按钮永久置灰、点了还会拿到 Action is disabled。{ source: ' ' } 那行额外把 visible 侧也带坏(归一成 '${ }',求值 falsy → 隐藏),与纯空白裸串在 #3850 之前的表现一致。

修法(一处,已有唯一定义)

#3850 已把定义下沉为 packages/core/src/evaluator/declaredPredicate.ts 的 hasDeclaredPredicate,三个消费者都改读它了,所以这里只需把「空」的判据从「裸串 trim」扩到「谓词文本 trim,不论它是裸串还是信封里的 source」:

function blankPredicateText(value: unknown): boolean {
  if (typeof value === 'string') return value.trim() === '';
  if (value && typeof value === 'object') {
    const src = (value as { source?: unknown }).source;
    if (typeof src === 'string') return src.trim() === '';
  }
  return false;
}

⛔ 不要在任何调用点补判定 —— 那是第 N 种拼法回到消费者,#3842 / #3849 / #3850 三单合掉的正是这类分身。

需要一并裁的一点:这一改会同时改到 #3848 已合入的执行门 —— disabled: { dialect: 'cel', source: ' ' } 从「拒执行」变成「执行」。方向与 #3848 自己的裁决一致(没有条件不该拦),但那面已经有钉子和 changeset,应作为行为变更明确写出而不是当作等价。另一个可选方向是在归一器处改(toPredicateInput 对信封改判 if (!src.trim()) return undefined),那样 evaluateCondition(toPredicateInput(x)) 的所有消费者一起对齐,但它改的是归一器契约(EvaluatorPredicateInput 现在声明 source: string 非空,#3074 / #4115 的概念分界),影响面更大 —— 两条路的取舍建议一起裁。

Related: #3850(前三种拼法的裁决与定义下沉,本卡是它枚举之外的第四种)、#3862 / #3955(SchemaRenderer 侧同族)、#3848 / #3872(执行入口两处门)、#3842 / #3849(动作面五处落点)、#3492(不变量出处)、#3957(ActionEngine 仍自带范围)。未认领。

Activity

  1. yinlianghui commented on Aug 9, 2026

    @yinlianghui
    ContributorAuthor

    PM 分诊(session_01GTRjn8xBqp75dk7kFupVRt):晋级 pm:queue,方向代裁 —— 「空」判据的 trim 语义扩到谓词信封的 source({ dialect, source: ' ' } 与三种既裁拼法同判「未声明」)。这不是新裁量,是 #3850 裁决「归一后仍有可求值条件才算已声明」在第四种拼法上的字面延伸;修改会同步触及 #3848 执行门(PR3873)已钉行为 —— 属预期跟随,修时一并更新那些钉与本单的 documented-residue 用例(其翻红正是修好的信号)。落点(定义处 vs toPredicateInput)由实施者按最小面选择并论证。

    排程:与 #3955、#3957 三单结对一 PR,Blocked-by PR #3966 —— 合入后即派。


    Generated by Claude Code

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

    @yinlianghui
    ContributorAuthor

    🔒 认领(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt,批次 4)—— 与 #3955、#3957 三单结对一 PR,分支与文件面声明见 #3955 认领评论(claude/issue-3955-3957-3960-predicate-scope-converge)。本单按分诊评论执行(trim 扩到信封 source,documented-residue 用例翻红即修好的信号,同步更新执行门的钉)。


    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