fix(spec): let a schemaless node own an expression-ledger entry (#4439) - #4474
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
#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
…on-routing-bug-aufqaj
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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 free
to 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.
Closes #4439. Follow-up to #4440 (#4414).
FLOW_NODE_EXPRESSION_PATHS是 #4027 立的账本 —— 告诉registerFlow和objectstack validate哪些 config 键装表达式、装的是哪种方言。它的棘轮从 descriptor 的configSchema推导预期,并且双向咬死:未登记的标记会红,没人声明的账本条目也会红。decision/script/subflow刻意不发 descriptorconfigSchema—— 发一份残缺的会把它们手写 Studio 表单需要的编辑器整个丢掉(#4210 事故)。两条规则一叠成了一个洞:schemaless 节点上的表达式槽位棘轮够不着,而反向那条又让它没法手工登记。decision.conditions[].expression就卡在这个洞里。它自己的 schema 写着 "Bare CEL predicate deciding this branch",自己的注释点名{…}是 #1491 陷阱,却没有任何校验器走它 —— 于是{lead_record.status} == 'converted'过了tsc、过了objectstack validate、过了注册。#4414 让它在运行时响亮失败;本 PR 让它在构建时失败,而那段延迟正是 #4027 存在的理由。改法
棘轮现在读两条声明通道:
configSchema—— 不变,从运行时注册表枚举;schemaless-node-config.zod.ts—— 标记走.meta({ xExpression })经z.toJSONSchema输出,和loop.collection自 objectui#2670 起用的是同一条通道。spec 把第二条以 JSON Schema 形式交出(
getSchemalessNodeConfigJsonSchemas(),memoized,input模式 —— 正好是 descriptorconfigSchema已有的形状),所以两条通道由同一个函数遍历:没有第二套「什么算已声明的表达式属性」(那正是账本要消除的重复),也不用给service-automation加zod依赖。两条通道各自单独断言非空,免得一侧推导坏掉躲在另一侧的结果后面。另外导出
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 的全部。这个精确形状已固化成常驻回归测试,两个方向都断言。
验证
@objectstack/lint(含 app-crm 那条谓词在registerFlow和objectstack validate两侧都被拒,报错定位到config.conditions[0].expression)。pnpm build+pnpm test整仓 132/132 全绿;pnpm typecheck(122 tasks)、pnpm lint、check:docs、check:generated干净。check:api-surface抓到 3 个新增导出(0 breaking),已gen:api-surface重新生成快照并提交。相关
configSchemaand the executor's wire payload are two unchecked lists — nothing validates flow node config keys at author time #4027(账本本身)、Console: screen-flow Submit never calls the resume endpoint — every screen flow is un-completable from the UI #3528(它要防的那个缺陷)service-automation/README.md教的是一套不存在的 flow DSL。不是本次改动造成的,单修其中一块会更不一致,故单开。)🤖 Generated with Claude Code
https://claude.ai/code/session_01Q8as8yR67v41xEdomiTba9
Generated by Claude Code