Skip to content

fix(spec): let a schemaless node own an expression-ledger entry (#4439) - #4474

Merged
os-zhuang merged 4 commits into
mainfrom
claude/app-crm-decision-routing-bug-aufqaj
Aug 1, 2026
Merged

fix(spec): let a schemaless node own an expression-ledger entry (#4439)#4474
os-zhuang merged 4 commits into
mainfrom
claude/app-crm-decision-routing-bug-aufqaj

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4439. Follow-up to #4440 (#4414).

FLOW_NODE_EXPRESSION_PATHS#4027 立的账本 —— 告诉 registerFlowobjectstack validate 哪些 config 键装表达式、装的是哪种方言。它的棘轮从 descriptor 的 configSchema 推导预期,并且双向咬死:未登记的标记会红,没人声明的账本条目也会红。

decision / script / subflow 刻意不发 descriptor configSchema —— 发一份残缺的会把它们手写 Studio 表单需要的编辑器整个丢掉(#4210 事故)。两条规则一叠成了一个洞:schemaless 节点上的表达式槽位棘轮够不着,而反向那条又让它没法手工登记

decision.conditions[].expression 就卡在这个洞里。它自己的 schema 写着 "Bare CEL predicate deciding this branch",自己的注释点名 {…}#1491 陷阱,却没有任何校验器走它 —— 于是 {lead_record.status} == 'converted' 过了 tsc、过了 objectstack validate、过了注册。#4414 让它在运行时响亮失败;本 PR 让它在构建时失败,而那段延迟正是 #4027 存在的理由。

改法

棘轮现在读两条声明通道:

  • descriptor configSchema —— 不变,从运行时注册表枚举;
  • schemaless-node-config.zod.ts —— 标记走 .meta({ xExpression })z.toJSONSchema 输出,和 loop.collection 自 objectui#2670 起用的是同一条通道。

spec 把第二条以 JSON Schema 形式交出(getSchemalessNodeConfigJsonSchemas(),memoized,input 模式 —— 正好是 descriptor configSchema 已有的形状),所以两条通道由同一个函数遍历:没有第二套「什么算已声明的表达式属性」(那正是账本要消除的重复),也不用给 service-automationzod 依赖。两条通道各自单独断言非空,免得一侧推导坏掉躲在另一侧的结果后面。

另外导出 SCHEMALESS_NODE_CONFIG_SCHEMAS 供其它需要枚举全部节点 config 契约的地方使用。纯增量 —— objectui 的 flow-node-config 对账按名字逐个 import,不受影响。

扫完了整个 schemaless 类

结论写进了账本头部,免得下次重推:script.template 是模板 id 不是模板体;script.inputs / script.variables / subflow.input 是插值 {token} 的值 —— 带洞的文本,几乎每个节点的 config 字符串都是这形状,已由 validate-flow-template-paths 和 CLI flow linter 泛化覆盖。flow-template 这个 role 指的是更窄的东西:必须解析成一个值的引用,比如 loop.collection。所以 decision 的谓词是这一类里唯一真正声明出来的表达式槽位。

修掉三处教错方言的文档

flows 指南在三处教 '{order_amount} > 10000',还有一条「decision 表达式漏花括号会永远走第一条分支」的警告 —— #4414 之后整个反过来了。FlowNodeSchema 自己的 @example 同样。全部改成裸 CEL 并写清历史,让构建突然失败的作者知道变了什么。方言表从三种降到两种:谓词永不带花括号,值永远带

顺带补上 #4414 changeset 漏掉的影响面

objectui 的 FlowEdgeInspector 一直在写 isDefault: true(把出边绑到 decision 的 default/else 分支时),写进一个零读者的键 —— 那条边于是无条件跑,和真正命中的分支并行。所以 enforcement 改的是存量 flow,而且主要是 Studio 自己产的:它们现在只走一条分支。这是修复,但它作用在已有数据上,读 release notes 的人升级前需要知道。

同一个 inspector 还把每个分支的 expression 和 label 抄到它接的边上 —— 也就是说 Studio 产出的正是指南里叫手写作者避免的「双重声明」。它是对的(designer 按构造保证两侧一致),所以指南改成如实说明,而不是禁止一个我们自己工具在产的形状:冗余但不错,只在两侧不一致时危险,而那正是 #4414 的全部。这个精确形状已固化成常驻回归测试,两个方向都断言。

验证

  • 15 个新增/更新测试,覆盖棘轮、引擎注册通道、@objectstack/lint(含 app-crm 那条谓词在 registerFlow objectstack validate 两侧都被拒,报错定位到 config.conditions[0].expression)。
  • pnpm build + pnpm test 整仓 132/132 全绿;pnpm typecheck(122 tasks)、pnpm lintcheck:docscheck:generated 干净。
  • check:api-surface 抓到 3 个新增导出(0 breaking),已 gen:api-surface 重新生成快照并提交。

相关

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9


Generated by Claude Code

claude added 2 commits August 1, 2026 08:08
`FLOW_NODE_EXPRESSION_PATHS` (#4027) tells `registerFlow` and `objectstack
validate` which config keys hold expressions and in which dialect. Its
ratchet derives what it expects from descriptor `configSchema` markers and
fails both ways — an undeclared marker, and a ledger entry nothing declares.

`decision` / `script` / `subflow` publish no descriptor `configSchema` on
purpose (a partial one would drop the editors their hand-written Studio
forms need — the #4210 incident). Those two rules compose into a hole: an
expression slot on a schemaless node is unreachable by the ratchet, and the
reverse direction then refuses to let it be entered by hand either.

`decision.conditions[].expression` sat in that hole. Its own schema calls it
a bare CEL predicate and its own comment names `{…}` as the #1491 trap, and
no validator walked it — so a braced predicate passed tsc, passed validate,
passed registration. #4414 made that loud at run time; this makes it a build
failure, which is the delay #4027 exists to remove.

The ratchet now reads both declaration channels. Spec hands the schemaless
one over as JSON Schema (`getSchemalessNodeConfigJsonSchemas()`, memoized,
input mode — the shape a descriptor's `configSchema` already is), so both are
walked by the same function: no second notion of "a declared expression
property", and no `zod` dependency added to service-automation. Each channel
is separately asserted non-empty so a broken derivation cannot hide behind
the other's results.

Swept the rest of the class and recorded the result in the ledger header:
`script.template` is a template id, not a body; `script.inputs` /
`script.variables` / `subflow.input` interpolate `{token}` like essentially
every node config string and are covered generically. The decision predicate
is the only genuinely declared expression slot there.

Docs: the flows guide taught the wrong dialect for decision predicates in
three places, plus a warning that inverted after #4414 — and FlowNodeSchema's
own `@example` did the same. Corrected to bare CEL with the history stated,
so an author whose build now fails knows what changed. The dialect table goes
from three dialects to two: predicates never take braces, values always do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9
…dio emits

Two gaps found while verifying #4439.

The #4414 changeset described the mechanism but not who it moves. `objectui`'s
FlowEdgeInspector has always written `isDefault: true` when an out-edge is
bound to a decision's default/else branch — into a key with zero readers, so
that edge ran unconditionally alongside whichever branch matched. Enforcement
therefore changes STORED flows, and mostly Studio's own: they now take exactly
one branch. That is the fix, but it lands on existing data, which a reader of
the release notes needs to know before upgrading.

The same inspector also copies each branch's expression and label onto the
edge it wires, so Studio emits the very double declaration the authoring guide
told hand-writers to avoid. It routes correctly — the designer keeps the two
sides in sync by construction — so the guide now says that plainly instead of
forbidding a shape our own tool produces: redundant, not wrong, and dangerous
only when the two disagree, which is the whole of #4414.

Pins that exact emitted shape as a regression test, both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9
@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 10:28am

Request Review

claude added 2 commits August 1, 2026 09:43
#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
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l 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 added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit f3141d8 Aug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/app-crm-decision-routing-bug-aufqaj branch August 1, 2026 11:01
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

表达式 ledger 只认 descriptor configSchema,所以「刻意 schemaless」的节点上声明的 CEL 槽位没有任何 build-time 校验器

2 participants