Repository navigation
toPredicateInput 对已经是 ${…} 模板的字符串二次包裹,动作面的谓词一律判成 true(visible 永远显示 / disabled 永远置灰) #3871
Description
Activity
Triage:
pm:queue— restore-invariant class (#3314 shape: same predicate value, opposite verdicts on different render paths), live repro in the card.- Anchor (read, not inferred): verified on
origin/main@56ff091—packages/core/src/evaluator/predicateInput.ts:90still wraps every string unconditionally, so an already-templated predicate gets double-wrapped and every action-surface verdict built throughtoPredicateInputcollapses to constant true (visible: always shown / disabled: always greyed). - Dedupe: no shadow. 「空谓词」在三处有三种范围:disabled: { dialect: 'cel', source: '' } 仍被判成已声明的门 → 永久置灰(#3842 修完后的残留,需先裁) #3850 and SchemaRenderer 的 disabled 门用第三种拼法 !== undefined:disabled: '' / disabled: null 在通用渲染路径上永久置灰 #3862 mention
toPredicateInputbut describe different defects (empty-predicate scope; SchemaRenderer's third spelling). No open PR touchespredicateInput.ts. - Stale-premise: card measured @
f0a625a; re-verified at current head after two forced main updates. - Direction note (non-binding): of the two paths the card lays out, the normalizer guard (treat an already-templated string as normalized) converges the action surfaces with the two green-pinned paths (SchemaRenderer expressions pin; page:header predicate-dialect pin) and needs no contract change. The alternative — declaring the template spelling off-spec on action predicates and rejecting loudly — would be an objectstack
packages/speccard (spec seat), not this repo's call. If the claimant finds evidence favoring rejection, surface it rather than implementing it here; otherwise the convergence path is the queue-ready fix. The ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848 tripwire pin will go red when this lands — update it as its note instructs.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Anchor (read, not inferred): verified on
⚖️ PM 裁决(维护者已授权代裁契约机械类,session
session_01GTRjn8xBqp75dk7kFupVRt):A 先落地(生产端修归一器,字符串已含${则原样返回),B(spec 发布期校验)转 spec 车道跟进依据:同一谓词值在 SchemaRenderer/page:header(有绿钉的正确行为)与动作面给出相反答案,正是 #3314 已付过代价的形状;A 把四个包收敛到既有钉子钉住的同一 verdict,是向已裁不变量的收敛而非新方向。C(固化错误成第二套契约)不可取。实施须带跨包回归清点(visible/disabled 在动作面对
${…}的 verdict 变更面逐点列出)+ 反向验证。B 属packages/spec面,按跨分片协议归主仓 PM —— 待 A 落地后由本席在 objectstack 立转办单。bug+pm:queue已挂,进入派发池(排在 #3240 空批次之后)。
Generated by Claude Code
- addedtarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)v17 发布窗口工作集(GA 前排查 2026-08-04)
on Aug 9, 2026 os-project-manager commented
on Aug 9, 2026 CollaboratorMore actionsRelease board:
target:v17applied — class ①,toPredicateInputre-wraps an already-normalised${…}template so every action-surface predicate written in the documented${…}spelling evaluates totrue:visiblealways shows an action the author meant to hide,disabledalways greys one out, across seven renderer call sites. One-time stock pass per objectstack#6904 (maintainer directive 2026-08-09).
Generated by Claude Code
🔒 认领(objectui 分片 PM,session
session_01GTRjn8xBqp75dk7kFupVRt,恢复批次 1)- 分支:
claude/issue-3871-predicate-double-wrap - 执行已裁 A 案(评论 5229247549):
toPredicateInput对已含${的字符串原样返回;实施带跨包回归清点(七个动作面调用点对${…}的 verdict 变更逐点列出)+ 反向验证;ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848 tripwire 按其注释更新。 - 文件面:
packages/core/src/evaluator/predicateInput.ts+ 各动作面测试 + ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848 tripwire 测试。B 案(spec 发布期校验)由 PM 落地后另立转办单,dev 不做。
Generated by Claude Code
- 分支:
- added 3 commits that reference this issue
on Aug 10, 2026
来源
发现自 #3848 实施(
ActionRunner.execute的 disabled 执行门)的探针对照。#3848 的派发形状原本是「先过toPredicateInput归一,再据归一结果求值」,逐形状实测时发现该复合对已经是${…}模板的字符串给出恒定true,于是 #3848 的执行门改为「归一只用于判门、verdict 仍读原始值」,并把这条单独立单。不在 #3848 修法面(那单只碰packages/core/src/actions/ActionRunner.ts),未认领。机理
packages/core/src/evaluator/predicateInput.ts的toPredicateInput对字符串无条件包裹:它假定入参是裸表达式(
record.done == true)。但${…}模板也是本仓文档在册的拼法(AGENTS.md §4 的hidden?: string; // expression: "${data.role != 'admin'}"),而EvaluatorPredicateInput这个类型自己就把「${…}模板字符串」列为合法的归一输出。于是一个已经归一好的值被再包一层:'${x}'→'${${x}}'ExpressionEvaluator.evaluate的单模板快路是/^\$\{([^}]+)\}$/,[^}]+跨不过内层的},匹配失败;落到全局 replace,内层表达式取到'${x',evaluateExpression抛错,catch 里return match原样退回 —— 最终结果是原字符串(非空),Boolean(非空字符串)=true。所以
evaluateCondition(toPredicateInput('${任意}'))恒为 true,与表达式本身的取值无关(只留下一条console.warn)。实测(worktree @
origin/main=f0a625aa7b00c93a48329e1c456b1aeacfceba43,一次性探针,未提交)context 为
{ user: { role: 'admin' } },同一个谓词两条路:即:裸表达式正常,
${…}拼法恒真。影响面(按
toPredicateInput的调用点)复合成
useCondition(toPredicateInput(x))/evaluateCondition(toPredicateInput(x))的落点都中招:packages/components/src/renderers/action/action-button.tsx(visible/disabled/enabled)action-icon.tsx、action-group.tsx(内联按钮 + 下拉项)、action-menu.tsx、action-bar.tsxpackages/app-shell/src/views/DeclaredActionsBar.tsxpackages/plugin-detail/src/renderers/record-quick-actions.tsx、RelatedList.tsxpackages/core/src/actions/ActionEngine.ts:237(getActionsForLocation的visible过滤)用户可见后果按键分两向:
visible: '${…}'→ 恒真 → 永远显示(该藏不藏);disabled: '${…}'→ 恒真 → 永远置灰(作者写什么都解不开)。ActionEngine 那处是throwOnError: true+ fail-closed,但双重包裹的失败发生在evaluate内部的 catch 里(return match),不抛,所以也是恒真=恒显示。对照:另两条路对同一拼法是对的(且有绿钉子)
packages/react的 SchemaRenderer 走原始值:packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx:135钉住disabled: '${data.status === "locked"}'在status: 'active'下不置灰。page:header走evalRowPredicate(packages/core/src/evaluator/listConditional.ts),legacy 拼法原样喂evaluateCondition:packages/components/src/__tests__/page-header-predicate-dialect.test.tsx:257钉住${…}取假时隐藏。所以这是 #3314 那个形状的又一实例:同一个谓词值,动作面渲染器与通用渲染路径给出相反答案。动作面这一侧的
${…}用例一个都没有(#3842/#3849 的钉子用的是裸表达式features.locked == true),所以缺陷一直没被 CI 看见。修法建议(未裁)
生产端一处改完即全线收敛 —— 已经是归一形态的字符串不该再包一次:
但请先裁:这会让上面所有落点对
${…}拼法从「恒真」变成「按取值判」,即visible: '${取假}'从「显示」变「隐藏」、disabled: '${取假}'从「置灰」变「可点」—— 方向都是正确化,但是跨 4 个包的 verdict 变更面,且与 SchemaRenderer / page:header 收敛。另一条路是判定${…}在动作谓词上off-spec(@objectstack/spec的PredicateInput只有裸串与 dialect envelope),那就该在发布期校验里响亮拒绝,而不是留在消费端静默恒真。两条路都比现状好,取舍属契约裁决。#3848 的执行门已按「归一只用于判门、verdict 读原始值」落地,并在钉子里留了指向本单的 tripwire(本单修好那天该 tripwire 会红,提示按本单更新)。
Related: #3848(执行门,本单发现出处)、#3850(「空谓词」范围的三处分歧)、#3862(SchemaRenderer 第三种拼法)、#3314(渲染路径与引擎路径判定分歧的先例)、#3521(page:header 的 legacy dialect 回退)。未认领。