Skip to content

fix(core): stop double-wrapping an already-${…} predicate, so action-face visible/disabled finally honour it (#3871) - #3939

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3871-predicate-double-wrap
Aug 9, 2026
Merged

fix(core): stop double-wrapping an already-${…} predicate, so action-face visible/disabled finally honour it (#3871)#3939
yinlianghui merged 1 commit into
mainfrom
claude/issue-3871-predicate-double-wrap

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes#3871

机理与修法(A 案,PM 裁决 5229247549)

packages/core/src/evaluator/predicateInput.tstoPredicateInput 是动作面与
动作引擎共用的唯一谓词归一器,它对字符串无条件包裹 ${…},假定入参是裸表达式。
${…} 既是本仓文档在册的谓词拼法(AGENTS.md §4),也是该函数自己的输出类型
EvaluatorPredicateInput 列为合法的形态之一 —— 于是一个已经归一好的值被再包一层:
'${x}' 变成 '${${x}}',单模板快路正则 /^\$\{([^}]+)\}$/[^}]+ 跨不过内层
},匹配失败落到全局 replace,内层表达式 '${x' 不可解析。作者写的表达式从此完全
不参与判定。

本 PR 执行已裁 A 案:已含 ${ 的字符串视为已归一,原样返回(同一个守卫也覆盖 envelope
分支 —— 那里 unwrap 出来的 source 一行之后落到完全相同的包裹,是同一缺陷的第二种拼法;
{ dialect: 'cel' } envelope 与裸表达式的行为完全不变)。归一器由此幂等

⚠️ 影响面清点:issue 正文的方向判断需要修正一处

issue 说所有落点都是「恒真」。实测(探针,origin/main @ 230ffd875,同一 context
同一谓词)显示恒定的方向由调用方的错误策略决定,而非一律恒真:

template TRUE | norm="${${user.role === \"admin\"}}" | raw=true | fail-soft=true | throwOnError=THREW
template FALSE | norm="${${user.role === \"guest\"}}" | raw=false | fail-soft=true | throwOnError=THREW
bare FALSE | norm="${user.role == \"guest\"}" | raw=false | fail-soft=false| throwOnError=false
  • fail-soft 调用点(不传 throwOnError)拿回未解析的原字符串,Boolean(非空串)=
    恒 true,与 issue 一致。
  • fail-closed 调用点(throwOnError: true)拿到的是抛错;各站点的 catch 把抛错
    转成「隐藏」,所以是恒隐藏,不是恒显示 —— 方向相反,而且更糟:作者用模板拼法
    gate 的动作,即使谓词取真也永远不出现,并且 fail-closed 的 warn 还会去指责作者的
    表达式。

两个方向都被本次修复收敛到「按取值判」。

七个调用点逐点 verdict 变更

#调用点错误策略修复前(与取值无关)修复后新钉子
1components/renderers/action/action-button.tsx:59visiblefail-closed恒隐藏按取值action-template-predicate-gate.test.tsx(action:button)
1同上 :69 / :70disabled / enabledfail-soft恒置灰 / 恒可点按取值同上
2action-icon.tsx:46visiblefail-soft恒显示按取值同上(action:icon)
2action-icon.tsx:50 / :51disabled / enabledfail-soft恒置灰 / 恒可点按取值同上
3action-group.tsx:73/78/79(内联成员)、:148/151/152(下拉项)visible / disabled / enabledfail-soft恒显示 / 恒置灰 / 恒可点按取值同上(inline member、dropdown member)
3action-group.tsx:206(宿主自身)visiblefail-soft恒显示按取值同上(action:group host)
4action-menu.tsx:74(成员)visiblefail-closed恒隐藏按取值同上(action:menu member)
4action-menu.tsx:81 / :82disabled / enabledfail-soft恒置灰 / 恒可点按取值同上
4action-menu.tsx:142(宿主)visiblefail-closed恒隐藏按取值由成员/宿主同一 hook 覆盖(见备注)
5action-bar.tsx:117(宿主)visiblefail-closed恒隐藏按取值同上(action:bar host)
6app-shell/views/DeclaredActionsBar.tsx:119visiblefail-closed恒隐藏按取值DeclaredActionsBar.test.tsx 新增两例
6同上 :128disabledfail-soft恒置灰按取值同上
7aplugin-detail/renderers/record-quick-actions.tsx:207disabledfail-soft恒置灰按取值record-quick-actions.template-predicate.test.tsx
7bplugin-detail/RelatedList.tsx:250visiblefail-soft恒显示按取值related-toolbar-visible.test.tsx 新增两例
8core/actions/ActionEngine.ts:237(getActionsForLocationvisible 过滤)visiblefail-closed恒隐藏(issue 记为恒显示,已修正)按取值ActionEngine.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-closed visible)浮出来的,disabled 又由
    渲染器 fail-soft 判定 —— 所以它是唯一一处能在同一次 mount 里观察到两个相反恒量的站点,
    新钉子把两半都钉了。
  • 另有第 8 个调用点 plugin-detail/renderers/record-alert.tsx:140(visible,
    fail-soft,恒显示 → 按取值),issue 未列。它同样被本次生产端修复治好,但它自己的测试
    (record-alert.test.tsx:36)把 toPredicateInputuseCondition 一起替成了 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 一行)—— 仍绿。

反向验证(方向先判后跑,含一处「反向」)

还原无条件包裹后,每个站点只有两例中的一例转红,红的那一例由该站点的错误策略决定:

  • fail-closed visible → 转红的是 ${取真} 应显示那一例(修复前恒隐藏);它的
    ${取假} 兄弟例在修复前就是绿的(隐藏就是隐藏,理由无从区分)。
  • fail-soft visible → 转红的是 ${取假} 应隐藏那一例。
  • disabled → 转红的是 ${取假} 应可点那一例。
  • enabled(取反腿)→ 转红的是 ${取假} 应置灰那一例。

因此每个站点都成对钉了两个极性,并在测试名/注释里标明哪一例是 detector、哪一例
「修复前也绿,但理由不对」—— 只钉 ${取假} 的写法会在五个 fail-closed 站点上以空理由
长绿。裸表达式钉子与上面两条既有绿钉在两个方向都保持绿。

实跑结果(把 wrapIfBare 临时改回无条件包裹,37 个测试转红,逐条与预判一致):

站点 / 用例${取真}${取假}
action:button visible(fail-closed)× 红(detector)✓ 绿(理由不对)
action:menu member visible(fail-closed)× 红(detector)✓ 绿(理由不对)
action:bar host visible(fail-closed)× 红(detector)✓ 绿(理由不对)
action:icon visible(fail-soft)✓ 绿(反变异守卫)× 红(detector)
action:group inline / dropdown / host visible(fail-soft)✓ 绿× 红(detector)
五个 leaf 的 disabled(fail-soft)✓ 绿× 红(detector)
五个 leaf 的 enabled(取反腿)✓ 绿× 红(detector)
DeclaredActionsBar visible / disabled2 例转红
record:quick_actions visible(经引擎)/ disabled2 例转红
RelatedList toolbar visible× 红(detector)
ActionEngine${…} describe2 例转红(含 warn 断言)
两处替换后的 tripwire各 1 例转红
predicateInput.test.ts16 例中 8 例转红(全部 ${…} 形态/verdict)
actionPredicate.parity 新增两行${取真} 行转红${取假} 行绿 —— parity 修复前是「两条路同错」而非同对
既有绿钉:SchemaRenderer.expressionspage-header-predicate-dialectplugin-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:253ActionRunner.conditionGate.test.ts:265)
按预测转红。它们的注释说「红了就删掉这条 tripwire,并且(只有那时)才可以让门去求值归一后的值」。

本 PR 的处理是替换而非删除,且不动执行门语义:

  1. 删掉会把「归一器幂等 ⇒ 归一值与原始值同判」这条修好的性质丢在依赖它的门旁边无人钉住,
    下次退回旧包裹只有生产端 suite 会红,读者也无从看出「归一只用于判门、verdict 读原始值」
    为什么仍然安全;
  2. 把门改成求值归一值,是在没有缺陷驱动的情况下改动已落地的 ActionRunner.execute 的 disabled 门把「空谓词」当已禁用,拦掉执行(实测 handler 不跑),且与渲染器判定不一致 #3848 / ActionRunner.execute 的 condition 门用真值判定:condition: false(最明确的「永不执行」)照样执行(实测 handler 跑了) #3872 语义 —— 两者
    现在对表内每一种形态都同判,改了纯属执行路径上的 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 skipped
  • pnpm exec vitest run packages/components/ packages/react/ packages/plugin-grid/src/__tests__/predicate-surface-parity.test.tsx --maxWorkers=2 → 137 files / 1271 passed
  • 反向验证与还原后复跑:49 files / 780 passed
  • pnpm exec turbo run type-check --concurrency=2 → 78 tasks successful, 0 error
  • node scripts/check-control-bytes.mjs → OK(扫 3833 个文本文件,零命中)

`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>
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 9, 2026 1:27pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-CRJey3X_.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)482.64KB106.39KB
core (index.js)3.00KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)139.61KB35.99KB
fields (index.js)231.13KB56.80KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.02KB30.47KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)236.63KB59.02KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)187.63KB49.66KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)109.67KB26.55KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)20.15KB6.72KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

验收通过(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)#3871(A 案,v17)

实物核验:base 230ffd875 → head 4d86a3aef,单提交 13 文件;wrapIfBare 同时守卫 string 与 envelope 两分支(后者是同一缺陷的第二拼法,超出 issue 但同根,采纳)。CI 亲读终态:20 检查全 completed、0 失败。

要点:

  • 方向修正采纳并有档案价值:六个 throwOnError 站点实为恒隐藏(throw→各站点 catch fail-closed),与 issue 的「恒显示」相反且更糟;fail-soft 站点才是恒真。verdict 变更清点表 8 个调用点(含 issue 未列的 record-alert.tsx:140)逐点两极性成钉,并在测试名里标明 detector 与「修复前也绿」的空理由例 —— #5046 陷阱的正确规避。
  • 既有绿钉(SchemaRenderer expressions 17 例、page-header dialect、plugin-grid 三面 parity)在变异下全绿 —— 收敛方向实证。
  • 两处裁量采纳:①tripwire 替换为收敛钉而非删除、执行门语义不动(注释是许可非义务,无缺陷驱动不改已落地门)—— 正确;②disabledGate parity 表在变异下仍绿的机理(静态转写表不调渲染器)如实declared并以活钉补位,0/{} 两行 divergence 归属改指 「空谓词」在三处有三种范围:disabled: { dialect: 'cel', source: '' } 仍被判成已声明的门 → 永久置灰(#3842 修完后的残留,需先裁) #3850 经查成立。
  • 反向验证 37 红逐条与预判一致。

范围外 finding #3941(record-alert 套件 stub 掉谓词管道,回归探测缺失)持有待分诊。B 案转办单由 PM 随后在 objectstack 立卡(spec 车道)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 13:41
@yinlianghui
yinlianghui added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit 1d723e3Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3871-predicate-double-wrap branch August 9, 2026 13:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

toPredicateInput 对已经是 ${…} 模板的字符串二次包裹,动作面的谓词一律判成 true(visible 永远显示 / disabled 永远置灰)

2 participants

@yinlianghui@claude