Uh oh!
There was an error while loading. Please reload this page.
fix(core): stop double-wrapping an already-${…} predicate, so action-face visible/disabled finally honour it (#3871) - #3939
Merged
Conversation
`toPredicateInput` — the one predicate normalizer `@object-ui/core` shares with
every action renderer and with `ActionEngine.getActionsForLocation` — wrapped
EVERY string as `${string}`, assuming a bare expression. But `${…}` is a
spelling this repo documents (AGENTS.md §4) and one the normalizer's own output
type lists as valid, so an already-normalized value was wrapped a second time:
`'${x}'` became `'${${x}}'`, which cannot match the evaluator's single-template
fast path (`[^}]+` stops at the inner `}`) and does not parse. The author's
expression then decided nothing, and which constant came back was set by the
caller's error policy:
- fail-soft legs (every `disabled` / `enabled`; `visible` on action:icon,
action:group, RelatedList, record:alert) got the unparsed string back, so
`Boolean(…)` was a constant true: greyed out forever / shown forever /
never disabling.
- fail-closed legs (`throwOnError: true` — `visible` on action:button,
action:bar, action:menu, DeclaredActionsBar, and the engine filter) got a
THROW, which each site turns into "hidden": an action gated with a template
predicate was invisible even while its gate held, and the fail-closed warn
blamed the author's expression. The issue card predicted constant-true
everywhere; this direction is its correction.
A string that already carries `${` is now returned untouched, via one
`wrapIfBare` helper so the bare-string branch and the envelope `source` branch
(the same defect's second spelling) cannot drift. The normalizer is idempotent,
and the action face converges on the verdict `SchemaRenderer` and `page:header`
have always reached for this spelling (#3314's shape). Bare expressions and
`{ dialect: 'cel' }` envelopes are unchanged.
Tests: a producer-side suite for the normalizer and both error policies, plus
live pins at every affected surface — five action leaves and the two hosts,
DeclaredActionsBar, record:quick_actions (the one surface where both opposite
constants are observable in one mount), the related-list toolbar and the engine
filter. Each surface pins BOTH polarities and labels which one is the detector,
because on a fail-closed site `${false}` was green before the fix too.
The two #3871 tripwires in `ActionRunner.{disabled,condition}Gate.test.ts` went
red as they predicted and are replaced by pins of the converged behaviour; the
execution gates keep reading the raw value (nothing forces the change now that
the two agree). `disabledGate`'s cross-face parity table claims the two `${…}`
rows it used to exclude, and the `0` / `{}` rows' divergence is re-attributed to
#3850, which owns the renderer's "is a gate declared?" scope — they never went
through the string branch.
Co-authored-by: Claude <noreply@anthropic.com>The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 9, 2026
CollaboratorAuthor
✅ 验收通过(objectui 分片 PM,session 实物核验:base 要点:
范围外 finding #3941(record-alert 套件 stub 掉谓词管道,回归探测缺失)持有待分诊。B 案转办单由 PM 随后在 objectstack 立卡(spec 车道)。 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 9, 2026 13:41
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3871
机理与修法(A 案,PM 裁决 5229247549)
packages/core/src/evaluator/predicateInput.ts的toPredicateInput是动作面与动作引擎共用的唯一谓词归一器,它对字符串无条件包裹
${…},假定入参是裸表达式。但
${…}既是本仓文档在册的谓词拼法(AGENTS.md §4),也是该函数自己的输出类型EvaluatorPredicateInput列为合法的形态之一 —— 于是一个已经归一好的值被再包一层:'${x}'变成'${${x}}',单模板快路正则/^\$\{([^}]+)\}$/的[^}]+跨不过内层},匹配失败落到全局 replace,内层表达式'${x'不可解析。作者写的表达式从此完全不参与判定。
本 PR 执行已裁 A 案:已含
${的字符串视为已归一,原样返回(同一个守卫也覆盖 envelope分支 —— 那里 unwrap 出来的
source一行之后落到完全相同的包裹,是同一缺陷的第二种拼法;{ dialect: 'cel' }envelope 与裸表达式的行为完全不变)。归一器由此幂等。issue 说所有落点都是「恒真」。实测(探针,
origin/main@230ffd875,同一 context同一谓词)显示恒定的方向由调用方的错误策略决定,而非一律恒真:
throwOnError)拿回未解析的原字符串,Boolean(非空串)=恒 true,与 issue 一致。
throwOnError: true)拿到的是抛错;各站点的 catch 把抛错转成「隐藏」,所以是恒隐藏,不是恒显示 —— 方向相反,而且更糟:作者用模板拼法
gate 的动作,即使谓词取真也永远不出现,并且 fail-closed 的 warn 还会去指责作者的
表达式。
两个方向都被本次修复收敛到「按取值判」。
七个调用点逐点 verdict 变更
components/renderers/action/action-button.tsx:59visibleaction-template-predicate-gate.test.tsx(action:button):69/:70disabled/enabledaction-icon.tsx:46visibleaction:icon)action-icon.tsx:50/:51disabled/enabledaction-group.tsx:73/78/79(内联成员)、:148/151/152(下拉项)visible/disabled/enabledaction-group.tsx:206(宿主自身)visibleaction:group host)action-menu.tsx:74(成员)visibleaction:menu member)action-menu.tsx:81/:82disabled/enabledaction-menu.tsx:142(宿主)visibleaction-bar.tsx:117(宿主)visibleaction:bar host)app-shell/views/DeclaredActionsBar.tsx:119visibleDeclaredActionsBar.test.tsx新增两例:128disabledplugin-detail/renderers/record-quick-actions.tsx:207disabledrecord-quick-actions.template-predicate.test.tsxplugin-detail/RelatedList.tsx:250visiblerelated-toolbar-visible.test.tsx新增两例core/actions/ActionEngine.ts:237(getActionsForLocation的visible过滤)visibleActionEngine.visibility.test.ts新增 describe备注:
action-menu.tsx:142宿主 gate 与:74成员 gate 用的是同一个useCondition(toPredicateInput(x), …, { throwOnError: true })复合;成员侧已有直接钉子,宿主侧的等价形状由
action:bar host的两例覆盖(action-bar.tsx:117与之逐字相同),没有为它单独造第三份 Radix 门面。
record-quick-actions这一面同时跑两条路:动作是经ActionEngine.getActionsForLocation(fail-closedvisible)浮出来的,disabled又由渲染器 fail-soft 判定 —— 所以它是唯一一处能在同一次 mount 里观察到两个相反恒量的站点,
新钉子把两半都钉了。
plugin-detail/renderers/record-alert.tsx:140(visible,fail-soft,恒显示 → 按取值),issue 未列。它同样被本次生产端修复治好,但它自己的测试
(
record-alert.test.tsx:36)把toPredicateInput与useCondition一起替成了 stub,该面的谓词判定从它的 suite 里根本观察不到,所以本 PR 不在那里加钉子 —— 按 Prime
Directive [WIP] Enhance every detail of the designer #10 另记为 finding(测试替身缺口),不在本 PR 修。
收敛方向的证明(既有绿钉保持绿)
同一拼法在这两条路上一直是对的,本次是把动作面收敛到它们:
packages/react/src/__tests__/SchemaRenderer.expressions.test.tsx:135(disabled: '${…}'取假时不置灰)—— 仍绿。
packages/components/src/__tests__/page-header-predicate-dialect.test.tsx:257(
${…}取假时隐藏)—— 仍绿。packages/plugin-grid/src/__tests__/predicate-surface-parity.test.tsx(三面 parity,含
par_legacy_template一行)—— 仍绿。反向验证(方向先判后跑,含一处「反向」)
还原无条件包裹后,每个站点只有两例中的一例转红,红的那一例由该站点的错误策略决定:
visible→ 转红的是${取真}应显示那一例(修复前恒隐藏);它的${取假}兄弟例在修复前就是绿的(隐藏就是隐藏,理由无从区分)。visible→ 转红的是${取假}应隐藏那一例。disabled→ 转红的是${取假}应可点那一例。enabled(取反腿)→ 转红的是${取假}应置灰那一例。因此每个站点都成对钉了两个极性,并在测试名/注释里标明哪一例是 detector、哪一例
「修复前也绿,但理由不对」—— 只钉
${取假}的写法会在五个 fail-closed 站点上以空理由长绿。裸表达式钉子与上面两条既有绿钉在两个方向都保持绿。
实跑结果(把
wrapIfBare临时改回无条件包裹,37 个测试转红,逐条与预判一致):${取真}${取假}action:buttonvisible(fail-closed)action:menumember visible(fail-closed)action:barhost visible(fail-closed)action:iconvisible(fail-soft)action:groupinline / dropdown / host visible(fail-soft)disabled(fail-soft)enabled(取反腿)DeclaredActionsBarvisible / disabledrecord:quick_actionsvisible(经引擎)/ disabledRelatedListtoolbar visibleActionEngine${…}describepredicateInput.test.ts${…}形态/verdict)actionPredicate.parity新增两行${取真}行转红${取假}行绿 —— parity 修复前是「两条路同错」而非同对SchemaRenderer.expressions、page-header-predicate-dialect、plugin-grid三面 parity一处需要点明的非预期方向:
disabledGate那张跨面 parity 表本身在变异下仍然绿—— 它比较的是
blocked与转写进表的rendererDisabled两列静态数据,不调用渲染器(该文件自己就写明了这一点:core 不能 import 渲染器包)。所以转写在变异下会变成谎言而
测试看不见;真正抓住它的是本 PR 在 components / app-shell / plugin-detail 新加的活钉子。
这正是把两行从
null改成声明 parity 时必须同时补活钉子的原因。ActionEngine那处另加了一条 warn 断言:fail-closed 隐藏是会 warn 的,所以「谓词取假而不 warn」才能把「判定为假」与「解析失败」两种隐藏区分开。
#3848 / #3872 tripwire 更新
两处 tripwire(
ActionRunner.disabledGate.test.ts:253、ActionRunner.conditionGate.test.ts:265)按预测转红。它们的注释说「红了就删掉这条 tripwire,并且(只有那时)才可以让门去求值归一后的值」。
本 PR 的处理是替换而非删除,且不动执行门语义:
下次退回旧包裹只有生产端 suite 会红,读者也无从看出「归一只用于判门、verdict 读原始值」
为什么仍然安全;
现在对表内每一种形态都同判,改了纯属执行路径上的 churn。注释给的是许可,不是义务。
同时更新了
disabledGate的跨面 parity 表:两行${…}从rendererDisabled: null(不声明 parity)变成声明 parity(
true/false),divergence 列表相应少两行,parity下限从 8 抬到 10。两行
0/{}的 divergence 归属从「#3871 family」改指 #3850 ——它们从不走字符串分支,与二次包裹无关,真正的分歧是渲染端「什么算已声明的 gate」的口径
(
hasDeclaredVisibilityGate(0)为真),那正是 #3850 的范围。B 案(spec 发布期响亮拒绝)不在本 PR
${…}该不该在动作谓词上可写是@objectstack/spec面的裁决(PredicateInput只建模裸串与 dialect envelope)。若要拒绝,应当在发布期校验里拒绝,而不是留给消费端静默
发明一个 verdict。PM 已说明 A 落地后由其在 objectstack 立转办单。
验证
pnpm exec vitest run packages/core/ packages/react/src/hooks/ packages/plugin-detail/ packages/app-shell/src/views/ --maxWorkers=2→ 334 files / 3937 passed, 1 skippedpnpm exec vitest run packages/components/ packages/react/ packages/plugin-grid/src/__tests__/predicate-surface-parity.test.tsx --maxWorkers=2→ 137 files / 1271 passedpnpm exec turbo run type-check --concurrency=2→ 78 tasks successful, 0 errornode scripts/check-control-bytes.mjs→ OK(扫 3833 个文本文件,零命中)