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.

「空谓词」在三处有三种范围:disabled: { dialect: 'cel', source: '' } 仍被判成已声明的门 → 永久置灰(#3842 修完后的残留,需先裁) #3850

Closed

Description

@yinlianghui

来源

发现自 #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 的概念分界)。未认领。

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue, with the requested ruling recorded here rather than parked in the decision box.

    Classification basis: measured residue with live anchors on origin/main @ 993336f (the #3842 merge): visibility-gate.ts:44 still judges declared-ness by != null && !== '' (objects always count), while predicateInput.ts:93 folds an empty-source envelope to undefined — the gap shape in the card's table is current. The envelope shape is the one objectstack build actually emits, so this is the most production-likely producer of the permanently-grey button.

    Direction ruling (triage): Option A is the default — widen hasDeclaredVisibilityGate's "empty" scope to match toPredicateInput (the repo's single normalizer, #3074/#4115 boundary), with the equivalence pins on the visible family the card demands. This is convergence on an already-ruled invariant (#3492 / #3842 line), not a new product semantic — measured behavior-equivalent for visible, and it only turns "permanently grey with no explanation" into "no gate" for disabled. The card's own ⛔ (no fourth local spelling at call sites) is binding. Option B (producer-side rejection of empty-source envelopes) is NOT queued and no upstream card is filed: that half changes the spec acceptance surface (framework repo, domain:spec red line) and has no demonstrated demand yet — per the 2026-08-08 scope-inflation discipline it waits for a maintainer ruling or a real occurrence; A alone closes the user-visible gap.

    Dedup: family-only (#3842 merged / #3848 core gate / #3849 five sites); no other card touches the shared definition.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. yinlianghui commented on Aug 8, 2026

    @yinlianghui
    ContributorAuthor

    裁决输入(来自 #3848 / PR #3873 的执行门实施,非认领,不改本单范围):

    执行门这一侧现在把「空谓词」取到了最宽的一档 —— '' + 纯空白 + 空 source envelope 全算「没有条件」→ 放行。实施时顺手量了一个对本单裁决有用的事实:纯空白这一行的 verdict 在两种可能的裁决下都是同一个,所以它不构成裁决压力:

    • 若裁「空白算空谓词(未声明)」→ 没有门 → 不禁用。
    • 若裁「空白算已声明的谓词,由取值决定」→ toPredicateInput(' ') 是 '${ }',evaluate 的单模板快路取到内层 ' ',evaluateExpression 对空串返回 undefined → Boolean(undefined) = false → 不禁用。

    两条路都落在「不禁用」。真正需要本单裁决的是本单标题那一行:{ dialect: 'cel', source: '' } 在渲染器侧仍被 hasDeclaredVisibilityGate(!= null && !== '')判成已声明的门,而它归一后是 undefined,evaluateCondition(undefined) 是 true,在 disabled 上即置灰 —— 这里两种裁决给出相反结果,躲不过去。

    另外一条与本单相邻但不同源的输入:归一器 toPredicateInput 对已经是 ${…} 模板的字符串二次包裹,使动作面渲染器把模板谓词一律判成 true(已另立 #3871)。若本单顺带收敛「谁来回答『有没有声明门』」,建议与 #3871 一起看 —— 前者是范围,后者是同一复合式里的归一正确性,两者都落在 evaluateCondition(toPredicateInput(x)) 这一个表达式上。

    #3848 的门为此刻意把归一只用于判门、verdict 仍读原始值,并保持 module-private(不新增第 N 个导出定义),等本单裁决落地后再改读共享定义。


    Generated by Claude Code

  3. yinlianghui commented on Aug 9, 2026

    @yinlianghui
    ContributorAuthor

    ⚖️ PM 裁决(维护者已授权代裁契约机械类,session session_01GTRjn8xBqp75dk7kFupVRt)—— 「空谓词」范围与定义放置一并裁定,与 #3862 结对实施

    1. 语义:「已声明门」= 归一后仍有可求值条件。''、纯空白、{ dialect, source: '' } 空 envelope 三种拼法一律不算已声明 —— 这不是新裁量:执行门(ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848/PR3873)已经按 toPredicateInput 归一落地了这个语义,本裁决只是让渲染器侧对齐,消灭 动作 disabled 的「已声明」判定用 != null,disabled: '' 把按钮永久置灰(#3492 同族的另一半 predicate,探针实证) #3842 残留的 envelope 置灰。
    2. 放置:唯一定义下沉 packages/core(与 toPredicateInput 同层 —— 它本来就是「归一后是否还有条件」这一问的家);components/.../visibility-gate.ts 保名 re-export(不重命名、不破既有导入面,五处 renderer 落点零改动);SchemaRenderer(SchemaRenderer 的 disabled 门用第三种拼法 !== undefined:disabled: '' / disabled: null 在通用渲染路径上永久置灰 #3862 的 !== undefined 第三拼法)与 ActionRunner 的 module-private helper(PR3873 注释里预告的收敛)一并改读。依赖方向成立:core 是 react/components 的共同下游。
    3. 顺序:toPredicateInput 对已经是 ${…} 模板的字符串二次包裹,动作面的谓词一律判成 true(visible 永远显示 / disabled 永远置灰) #3871(归一器二次包裹)先行或同批 —— 否则 ${…} 在动作面的恒真会污染「归一后可求值」的判定面。

    #3850 与 #3862 一 PR 结对,pm:queue 生效,排在 #3240 空批次之后。


    Generated by Claude Code

  4. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Release board: target:v17 applied — class ①, { dialect, source: '' } is exactly the envelope objectstack build compiles when an author leaves a predicate empty, and hasDeclaredVisibilityGate counts it as a declared gate while evaluateCondition answers true, so the button is permanently greyed with no way to tell it from the metadata's intent. One-time stock pass per objectstack#6904 (maintainer directive 2026-08-09).


    Generated by Claude Code

  5. self-assigned this
    on Aug 9, 2026
  6. yinlianghui commented on Aug 9, 2026

    @yinlianghui
    ContributorAuthor

    🔒 认领(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt,恢复批次 4)—— 与 #3862 结对,一 PR 双 Fixes(2026-08-09 01:50 PM 裁决评论既定)。


    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

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions