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

claude added 3 commits August 1, 2026 07:47
…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

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 1, 2026 8:05am

Request Review

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

github-actions Bot commented Aug 1, 2026

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 main Aug 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

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