来源
发现自 #3842 实施时对残留形状的探针核对(#3842 的修法只覆盖 '' 字面量)。修法要动的是 visible 一族共用的那处定义,属于要先裁的契约变更,故独立成单,未认领。
机理
「这是个空谓词吗」在仓里被回答了三次,范围各不相同:
| 定义 |
位置 |
判为「空」的范围 |
hasDeclaredVisibilityGate |
packages/components/src/renderers/action/visibility-gate.ts:43 |
null / undefined / '' |
toPredicateInput |
packages/core/src/evaluator/predicateInput.ts:87 |
null / undefined / '' / source 为空的信封 |
evaluateCondition |
packages/core/src/evaluator/ExpressionEvaluator.ts:237,246 |
上面全部 + 纯空白串(trim() 后为空) |
disabled 侧的置灰判定是「门已声明 且 求值为真」。两个定义范围不等,夹缝里的形状就是「门说已声明、求值说没有条件(→ true)」→ 永久置灰。
实测(worktree @ origin/main = aca561a77fd0dc793ea76a7b46cd037e1731eb93 + #3842 的 PR,一次性探针,未提交)
shape | 已声明? | toPredicateInput | evaluateCondition | disabled 侧
'' | false | undefined | true | 可点 ← #3842 已修
' '(纯空白) | true | "${ }" | false | 可点 ← 良性,见下
{dialect:'cel', source:''} | true | undefined | true | 置灰 ← 残留
{source:''}(无 dialect) | true | undefined | true | 置灰 ← 残留
true | true | true | true | 置灰(正确)
false | true | false | false | 可点(正确)
undefined | false | undefined | true | 可点(正确)
两个结论都是量出来的,不是推的:
为什么信封形状值得管
{ dialect, source } 正是 @objectstack/spec 的 ExpressionInputSchema 对任何已授权谓词(含裸串)归一后的产物,也是 objectstack build 编译出的形状。「作者留空 → 编译出 source: '' 的信封」是这条链上最自然的产生路径,比手写 disabled: '' 更可能出现在真实元数据里。'' 字面量反而更像是手写 JSON / 进程内构造才有的拼法。
同一形状在 visible 侧良性(判成已声明 → 求值 true → 显示,与「没有门」同一结果),所以只有 disabled 方向亮 —— 与 #3842 完全同一个不对称。
需要先裁(两条轴)
修法只有一处合理落点(hasDeclaredVisibilityGate 那唯一定义),但它被 visible 一族六处以上落点、三个包共用,改的是契约范围而不是一个调用点,所以不该由实施者顺手定。
选项 A —— 把「空」的范围在定义处对齐(推荐):hasDeclaredVisibilityGate 也把「source 为空的信封」判成「没有门」,即与 toPredicateInput 的范围一致。
选项 B —— 在 producer 侧拒收:让 spec / 发布期校验直接拒绝空 source 的谓词信封,渲染侧不动。
- 长期健康度:更契约优先 —— 一个求不出值的谓词本就不该被发布,与「declared = enforced」一致。
- 让 AI 写的元数据难写错:最强 —— 发布期大声拒绝,而不是运行期靠消费者宽容。
- 代价:落在
@objectstack/spec(framework 仓),跨仓;且对已在库里的历史元数据不追溯,渲染侧仍需要一个兜底姿态,所以大概是 A + B 而不是 B 单独成立。
⛔ 不推荐:在两处 disabled 调用点加一个「顺便判一下空 source」的本地判定 —— 那就是把第四种「空」的拼法写进消费者,#3842 刚刚合掉的正是这类分身。
我的建议:A 先落(收敛范围、visible 侧补等价钉子),B 作为 framework 侧独立单跟进。
影响
命中条件:元数据里出现 source 为空的谓词信封。表现与 #3842 一致 —— 按钮永久置灰,界面上无法与「元数据本意」区分。今天必然可见的回归尚未观察到,严重度请分诊裁。
Related: #3842(同族,'' 字面量半边已修,PR 分支 claude/issue-3842-disabled-declared-gate)、#3848(执行入口的同族半边,那侧纯空白串也被拦)、#3849(其余五处渲染落点)、#3492(不变量出处)、#3074 / #4115 upstream(PredicateInput 与 EvaluatorPredicateInput 的概念分界)。未认领。
来源
发现自 #3842 实施时对残留形状的探针核对(#3842 的修法只覆盖
''字面量)。修法要动的是visible一族共用的那处定义,属于要先裁的契约变更,故独立成单,未认领。机理
「这是个空谓词吗」在仓里被回答了三次,范围各不相同:
hasDeclaredVisibilityGatepackages/components/src/renderers/action/visibility-gate.ts:43null/undefined/''toPredicateInputpackages/core/src/evaluator/predicateInput.ts:87null/undefined/''/source为空的信封evaluateConditionpackages/core/src/evaluator/ExpressionEvaluator.ts:237,246trim()后为空)disabled侧的置灰判定是「门已声明 且 求值为真」。两个定义范围不等,夹缝里的形状就是「门说已声明、求值说没有条件(→true)」→ 永久置灰。实测(worktree @
origin/main=aca561a77fd0dc793ea76a7b46cd037e1731eb93+ #3842 的 PR,一次性探针,未提交)两个结论都是量出来的,不是推的:
toPredicateInput(' ')走的是字符串腿,包成'${ }',模板求值出 falsy,所以落在「不置灰」。不是因为哪一处判它为空。(它在执行入口ActionRunner那侧不良性,见 ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848。)source为空的信封是真残留:toPredicateInput明确把它折成undefined(if (!src) return undefined),而hasDeclaredVisibilityGate只看!= null && !== '',对象一律算已声明。为什么信封形状值得管
{ dialect, source }正是@objectstack/spec的ExpressionInputSchema对任何已授权谓词(含裸串)归一后的产物,也是objectstack build编译出的形状。「作者留空 → 编译出source: ''的信封」是这条链上最自然的产生路径,比手写disabled: ''更可能出现在真实元数据里。''字面量反而更像是手写 JSON / 进程内构造才有的拼法。同一形状在
visible侧良性(判成已声明 → 求值true→ 显示,与「没有门」同一结果),所以只有disabled方向亮 —— 与 #3842 完全同一个不对称。需要先裁(两条轴)
修法只有一处合理落点(
hasDeclaredVisibilityGate那唯一定义),但它被visible一族六处以上落点、三个包共用,改的是契约范围而不是一个调用点,所以不该由实施者顺手定。选项 A —— 把「空」的范围在定义处对齐(推荐):
hasDeclaredVisibilityGate也把「source为空的信封」判成「没有门」,即与toPredicateInput的范围一致。locations的动作会渲染到所有位置 —— 聚合批量动作(execution: 'aggregate')因此在列表工具栏留下一个必然失败的按钮 #3142 那类漂移的温床。对visible侧逐一推演是行为等价的(那侧空谓词求值为true= 显示 = 没有门),所以是纯收敛,不是给一族改行为。source时,渲染侧与归一侧同意「没有条件」,不会出现「按钮永久置灰但没人能解释为什么」。visible一族补上该形状的钉子(证明等价而不是假设),并处理hasDeclaredVisibilityGate那段文档 —— 它现在把!= null && !== ''写成 fix(plugin-grid): 批量操作按钮忽略 requiredPermissions,且布尔 visible 被判为故障导致按钮全员隐藏 #3492 的原话。选项 B —— 在 producer 侧拒收:让 spec / 发布期校验直接拒绝空
source的谓词信封,渲染侧不动。@objectstack/spec(framework 仓),跨仓;且对已在库里的历史元数据不追溯,渲染侧仍需要一个兜底姿态,所以大概是 A + B 而不是 B 单独成立。⛔ 不推荐:在两处
disabled调用点加一个「顺便判一下空source」的本地判定 —— 那就是把第四种「空」的拼法写进消费者,#3842 刚刚合掉的正是这类分身。我的建议:A 先落(收敛范围、
visible侧补等价钉子),B 作为 framework 侧独立单跟进。影响
命中条件:元数据里出现
source为空的谓词信封。表现与 #3842 一致 —— 按钮永久置灰,界面上无法与「元数据本意」区分。今天必然可见的回归尚未观察到,严重度请分诊裁。Related: #3842(同族,
''字面量半边已修,PR 分支claude/issue-3842-disabled-declared-gate)、#3848(执行入口的同族半边,那侧纯空白串也被拦)、#3849(其余五处渲染落点)、#3492(不变量出处)、#3074 / #4115 upstream(PredicateInput与EvaluatorPredicateInput的概念分界)。未认领。