来源
发现自 #3835 实施(DeclaredActionsBar 的 visible 真值门)的同文件清扫。不在 #3835 的修法面(PM 已裁其范围为 visible 门 + barrel re-export),故独立成单,未认领。
机理
visible 一族已收敛到 hasDeclaredVisibilityGate(!= null && !== '' —— 空谓词不算门),但同一形状的 disabled 判定停在 != null,少了 !== '' 那一半:
packages/app-shell/src/views/DeclaredActionsBar.tsx:
disabled={((action as any).disabled != null ? isDisabledPred : false) || loading}
packages/components/src/renderers/action/action-button.tsx:164:
(schema as any).disabled != null ? isDisabled : schema.enabled != null ? !isEnabled : false
于是 disabled: '' 被判成「声明了 disabled 门」,verdict 交给求值入口 —— 而求值入口对空谓词的语义是「没有条件 → true」:
toPredicateInput('') → undefined → evaluateCondition(undefined) → true。
对 visible 这条链上 true 意味着「显示」(所以 '' 在 visible 侧是良性的、双重保护),对 disabled 则反向:true 意味着禁用。空谓词从「没有门」变成「永久置灰」。
方向对照:visible: '' → 渲染(正确);disabled: '' → 禁用(错误)。同一个「空谓词」,两个 key 得到相反待遇。
实证(origin/main@14c59c0b969c8324160071d447a4b99492dfc35f + #3835 分支,一次性探针,未提交)
DeclaredActionsBar 挂 location: 'record_section' 的声明动作,分别带 disabled: '' / 不带 disabled / disabled: false:
FAIL PROBE 3835 declared `disabled` > disabled:'' should NOT disable the button (empty predicate is no gate)
Error: expect(element).not.toBeDisabled()
Received element is disabled:
button
data-testid="declared-action-approval_approve"
disabled=""
type="button"
variant="outline"
/
Tests 1 failed | 2 passed (3)
两个对照组(无 disabled、disabled: false)都是 enabled —— 坏的只是「已声明」的判定,不是求值。
action-button.tsx:164 是逐字相同的拼法,未单独探针(推断同形,请实施者自行验证后再改)。
影响
修法(与同族一致,应无新决定)
disabled 的「已声明」判定复用同一处命名定义(hasDeclaredVisibilityGate 的语义,或按 predicate-key 中立地重命名/新增一个 hasDeclaredPredicateGate,同一实现),两处一起改;enabled 那条 legacy 分支同理需要判断。钉子按同族对称性:'' 不置灰 / true 置灰 / false 不置灰 / 未声明不置灰,并区分 disabled 与 enabled 两条腿。
⚠️ 与 #3835 的 PR 同文件同区域(DeclaredActionsBar.tsx 的按钮 props / 门附近):建议等 #3835 落 main 后再派,或在派发时说明合并顺序。命名若要中立化(hasDeclaredVisibilityGate → 通用名),那是一次跨包重命名(components barrel + plugin-grid + app-shell),属于要先裁的小决定。
Related: #3835(发现来源、visible 侧第五处)、#3492(不变量出处)、#3758 / PR #3816、#3812 / PR #3825、#3823 / PR #3836、#1885(disabled 首次接线)。未认领。
来源
发现自 #3835 实施(
DeclaredActionsBar的visible真值门)的同文件清扫。不在 #3835 的修法面(PM 已裁其范围为visible门 + barrel re-export),故独立成单,未认领。机理
visible一族已收敛到hasDeclaredVisibilityGate(!= null && !== ''—— 空谓词不算门),但同一形状的disabled判定停在!= null,少了!== ''那一半:packages/app-shell/src/views/DeclaredActionsBar.tsx:disabled={((action as any).disabled != null ? isDisabledPred : false) || loading}packages/components/src/renderers/action/action-button.tsx:164:(schema as any).disabled != null ? isDisabled : schema.enabled != null ? !isEnabled : false于是
disabled: ''被判成「声明了 disabled 门」,verdict 交给求值入口 —— 而求值入口对空谓词的语义是「没有条件 → true」:toPredicateInput('')→undefined→evaluateCondition(undefined)→true。对
visible这条链上true意味着「显示」(所以''在visible侧是良性的、双重保护),对disabled则反向:true意味着禁用。空谓词从「没有门」变成「永久置灰」。方向对照:
visible: ''→ 渲染(正确);disabled: ''→ 禁用(错误)。同一个「空谓词」,两个 key 得到相反待遇。实证(
origin/main@14c59c0b969c8324160071d447a4b99492dfc35f+ #3835 分支,一次性探针,未提交)DeclaredActionsBar挂location: 'record_section'的声明动作,分别带disabled: ''/ 不带disabled/disabled: false:两个对照组(无
disabled、disabled: false)都是 enabled —— 坏的只是「已声明」的判定,不是求值。action-button.tsx:164是逐字相同的拼法,未单独探针(推断同形,请实施者自行验证后再改)。影响
DeclaredActionsBar的宿主是审批收件箱记录区动作条(apps/console/src/pages/system/ApprovalsInboxPage.tsx:2014/:2055),动作 def 来自服务端 metadata。一个disabled: ''的审批动作渲染成永久不可点的 Approve/Reject —— 与 app-shell/DeclaredActionsBar: 服务端声明动作的真值可见性门实证可达 ——visible: false照样渲染(#3823 消费半径清扫发现的同族第五处) #3835 的visible: false相反的失败方向:那边是「本该隐藏却可点」,这边是「本该可点却置灰」,都无法从界面上分辨是元数据意图还是 bug。disabled: ''(空字符串谓词,例如授权表单留空、模板求值成空串)才会命中;不是今天必然可见的回归,严重度请分诊裁。修法(与同族一致,应无新决定)
disabled的「已声明」判定复用同一处命名定义(hasDeclaredVisibilityGate的语义,或按 predicate-key 中立地重命名/新增一个hasDeclaredPredicateGate,同一实现),两处一起改;enabled那条 legacy 分支同理需要判断。钉子按同族对称性:''不置灰 /true置灰 /false不置灰 / 未声明不置灰,并区分disabled与enabled两条腿。DeclaredActionsBar.tsx的按钮 props / 门附近):建议等 #3835 落 main 后再派,或在派发时说明合并顺序。命名若要中立化(hasDeclaredVisibilityGate→ 通用名),那是一次跨包重命名(components barrel + plugin-grid + app-shell),属于要先裁的小决定。Related: #3835(发现来源、
visible侧第五处)、#3492(不变量出处)、#3758 / PR #3816、#3812 / PR #3825、#3823 / PR #3836、#1885(disabled首次接线)。未认领。