Skip to content

fix(automation): evaluateCondition decides the dialect from the source, not from the caller (#4336) - #4453

Merged
os-zhuang merged 3 commits into
mainfrom
claude/bare-string-flow-conditions-cel-982coi
Aug 1, 2026
Merged

fix(automation): evaluateCondition decides the dialect from the source, not from the caller (#4336)#4453
os-zhuang merged 3 commits into
mainfrom
claude/bare-string-flow-conditions-cel-982coi

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#4336

AutomationEngine.evaluateCondition按信封选引擎的:只有 { dialect, source } 才走 CEL,裸字符串一律落到 legacy {var} 模板路径,把两边当文本比。不报错,run 记 success,而且失败方向取决于谓词本身:

传进去的实际求值结果
existingTask == null'existingTask' === 'null'false —— 闸门永不打开
record.rating >= 4'record.rating' >= '4''r' > '4'true —— 分支焊死打开

#4440 的分工

#4440 已经把唯一一个撞上这个求值器的内置调用点修了 —— decision 执行器现在自己包 { dialect: 'cel', source } 再调。本 PR 修的是求值器本身,这样下一个调用点不必再记得包:方言改由 source 判定,条件里没有 {var} 洞就是 CEL。

这不是重复劳动,理由是 evaluateConditionAutomationEngine 上的公开方法(在 dist/index.d.ts 里)。一个 plugin 注册的节点执行器写 engine.evaluateCondition(cfg.when, vars),拿到的就是上面那张表,没有任何东西提醒它。#4440 之所以要在调用点手工包,正是因为求值器的契约本身是错的 —— 按 Prime Directive #12,该修的是那一份契约,而不是让每个调用点各修一遍。

合并 #4440 之后,仓库内已经没有调用点会走到 legacy 路径(边条件、start 门、decision 三处都携带信封),所以运行时爆炸半径确实小 —— 价值在契约与公开 API 这一层,以及下面这三个仍然存在的求值缺陷。

legacy {var} 方言:保留,但补齐

{amount} > 100{status} == active{a.b} == 7 行为完全不变。补上它缺的两件事:

  • 带引号字面量按内容比。{status} == 'active' 之前是拿 active 去比 'active'(连引号一起),对任何 status 取值都是 false。这正是 flow 文档给 decision 节点示范的写法,而且「字符串字面量要加引号」是平台上其他每一个谓词面都要求的。
  • 解析不出来不再回落 false 两条出口按 ADR-0032 §1c 改成带 source 和肇事引用的拒绝:
    • {…} 洞匹配不到任何流程变量 —— 比如 {lead_record.status}get_recordoutputVariable 存的是整行,所以这个键根本不存在。(不对称之处也写进了报错:节点输出会以 ${node.id}.${key} 扁平写进变量表,所以 {get_lead.id} 能替换成功,看起来像是这个写法能用。)
    • 替换完既不是布尔、也不是数字、又没有比较运算符 —— 无运算符的 truthy 门。

这两条正是 @os-zhuang 在 issue 里实测列出的「还剩两条静默 false」。

dialect: 'cel' 信封里的花括号仍是 #1491 陷阱、照 throw —— 显式写方言就是作者在说「这是 CEL」。方言嗅探跳过字符串字面量,所以 record.label == '{pending}' 还是 CEL,比的是字段。

有意的收紧

裸字符串只要不是合法 CEL 就会抛错,而之前它会字符串比较出某个答案。安全用例(process.exit(1) / require("fs")... / 箭头函数 IIFE)也归入这一类:两条路都从不执行宿主代码 —— CEL 里没有 process、没有 require、没有箭头函数语法 —— 但现在是一个被报告的 fault,不再是静默 falseengine.test.ts 的口径已跟着改。

文档

未纳入范围

decision.conditions[].expression 目前仍无构建期校验:它不在 FLOW_NODE_EXPRESSION_PATHS 账本里,而账本对账是双向的 —— 要加就得给 decision 描述符补 configSchema,那会顺带触发 validateNodeConfigKeys 去拒绝 decision 上的 config.condition。这正是 #4439 记录的那个结构性缺口(刻意 schemaless 的节点类型进不了账本)。另外它会让构建比运行时更严(构建拒 {…},运行时仍支持),那是一次需要 conversion 配套的、有计划的方言下线。

验证

  • 9 个新引擎测试,覆盖 issue 表格两行 + os-zhuang 补的两条静默 false + 带引号字面量 + 字符串字面量里的花括号 + 空条件。
  • 重写了 nested-region-parity.test.ts 里的 applyConversionsToFlow does not recurse into loop bodies — conditions inside a loop are never converted to CEL and the gate silently never opens #4347 块:裸的点号谓词现在正确求值而不是被拒(oppRecord.amount > 500000 在 amount=10 时为 false);拒绝仍然覆盖「点号引用混进 {…} 条件里」的情形。
  • pnpm test 全绿(132/132 tasks);service-automation 586 passed。
  • tsc 错误数与 main 基线一致(5,全在未触碰的行 —— 该包是 type-check 覆盖率账本里的 DEBT 条目)。
  • check:doc-authoringpnpm --filter @objectstack/spec check:docs 通过。

相关

🤖 Generated with Claude Code

https://claude.ai/code/session_011n4UBkyRZsy6CJqmKg6oA5


Generated by Claude Code

…t string-compared (#4336)
`evaluateCondition` picked its engine by asking whether an `{ dialect, source }`
envelope was present. A condition authored as a plain string therefore never
reached the CEL engine: it fell through to the legacy `{var}` template path,
which substitutes brace holes and then compares the leftover text AS TEXT.
Nothing errored and the run was recorded as `success`, with the failure
direction depending on the predicate:
'existingTask == null' -> 'existingTask' === 'null' -> always false
'record.rating >= 4' -> 'record.rating' >= '4' -> always true
One gate that never opens, one branch pinned open. Author-side discipline could
not fix the second half of this: `FlowNodeSchema.config` is an open `z.record`,
so no schema transform reaches a `decision` node's `conditions[].expression`,
which is exactly where bare-string conditions still live after `registerFlow`
parses edges into envelopes.
The dialect is now decided by the SOURCE, not by the envelope: a condition is
CEL unless it actually contains a `{var}` hole. The same predicate then
evaluates the same way wherever it is authored -- edge, start-node gate, or
decision node.
The `{var}` dialect keeps working where it always did, and gains the two things
it was missing:
* a quoted literal compares as its contents. `{status} == 'active'` used to
compare `active` against `'active'` -- quotes included -- and was false for
every value. It is the spelling the flow docs show.
* it no longer answers `false` when it could not resolve something. A hole
naming no variable (`{lead_record.status}` -- `get_record` stores the whole
row under one name, so that key never exists) and a substituted value that
is neither boolean, numeric, nor part of a comparison are refused with the
source and the offending reference attached, per ADR-0032 1c.
Braces inside an explicit `dialect: 'cel'` envelope remain the #1491 brace-trap
and still throw: stating the dialect is the author saying "this is CEL". The
sniff reads the source outside string literals, so `record.label == '{pending}'`
stays CEL and compares the field.
Also updated: app-crm's convert-lead guard (the real instance of the unresolvable
brace spelling -- its sibling EDGE already carried the bare-CEL form), the flow
docs' three-dialect table and its now-inverted "braces missing in a decision
expression" warning, and the `FlowNodeSchema` decision example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011n4UBkyRZsy6CJqmKg6oA5
#4440 landed the decision-node half from the other side: its executor now wraps
`conditions[].expression` in a CEL envelope at the call site, and app-crm's
guard became a plain exclusive gateway with no `config.conditions` at all.
Resolved:
* examples/app-crm/convert-lead.flow.ts — took main's version outright. The
block this branch corrected no longer exists there.
* engine.ts — both sides are additive at the top of the module (this branch's
template-hole/quoted-literal helpers, main's DEFAULT_BRANCH_LABEL); kept
both.
* flows.mdx — kept both edits (main rewrote the edge table and added the
branching section; this branch rewrote the expression-dialect table), and
re-attributed the decision-expression note to #4414 + #4336 rather than to
this branch alone.
Also fixed the "Basic Structure" example, which #4440 left carrying
`'{order_amount} > 10000'`: with decision expressions now routed as CEL that
spelling is the brace-trap and throws.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011n4UBkyRZsy6CJqmKg6oA5
…cision call site
The decision executor wraps its expression in a CEL envelope as of #4440, so
this branch is no longer what makes a decision predicate evaluate. Say what it
actually does: fix the evaluator's own dialect decision (public API — a
plugin-registered executor still hits the reported table), close the legacy
path's two silent-`false` exits, and fix the quoted-literal comparison.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011n4UBkyRZsy6CJqmKg6oA5
@vercel

vercelBot commented Aug 1, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 1, 2026 8:05am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/service-automation, @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 68c02c2 into mainAug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/bare-string-flow-conditions-cel-982coi branch August 1, 2026 08:23
os-zhuang pushed a commit that referenced this pull request Aug 1, 2026
#4453 landed the same class of fix from the other end: `evaluateCondition`
now sniffs the dialect from the source instead of from the caller, so a bare
string is CEL unless it holds a `{var}` hole. Three overlaps to reconcile:
- The flows guide's "Expressions in flows" section was rewritten on both
sides. Kept main's — it is more complete about what the `{var}` dialect
now does — and folded in the one fact it cannot know: a decision's
`conditions[].expression` is on the expression ledger as a predicate, so a
braced spelling there is a build failure, not the `{var}` dialect. Main's
"the `{var}` form still works" is true of a start node's plain-string
condition and would have been wrong for a decision.
- `FlowNodeSchema`'s `@example` likewise. Kept both branches from main and
dropped its `// default` annotation on the `true` catch-all: since #4414
the default path is the `isDefault` out-edge, not a catch-all branch.
- The decision executor's explicit `dialect: 'cel'` envelope is now
redundant for the case it was added for, but it is what keeps run time
agreeing with the validators: the sniff would route a braced predicate to
the template dialect and run it, while `registerFlow` / `objectstack
validate` reject exactly that spelling. Comment rewritten to say so —
the old one described a path that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bare-string flow conditions bypass the CEL engine and silently string-compare — wrong branches, no error

2 participants

@os-zhuang@claude