Skip to content

registerFlow's three validators still walk top-level nodes only — a region's malformed structure, unknown node type and undeclared config key all pass registration #4389

Description

@os-zhuang

#4347 / #4381 的后续。那次修掉了「元数据随嵌套深度而变」的转换谓词校验两半:

  • mapFlowNodes 递归进 region,ADR-0087 转换表能到达嵌套节点;
  • 新增 collectFlowGraphs,引擎的 validateFlowExpressions 和 lint 的 validateStackExpressions 都改成遍历它。

registerFlow另外三个校验器仍然只遍历 flow.nodes,同一个形状的洞还留着:ADR-0031 容器把整张子图放在自己开放的 config 里,校验器停在容器边界,于是「校验了一部分、却按整个 flow 汇报」。

三个缺口

校验器 位置 失败方式 region 里的现状
validateControlFlow spec/src/automation/control-flow.zod.ts hard-fail 只验顶层容器。嵌套容器(loop body 里再放一个 loop)畸形时注册照过,要到运行时 findRegionEntry 才抛 —— 违背 ADR-0031 的「reject the malformed before it can run」
validateNodeTypes engine.ts:2732(flow.nodes.map(...)) soft-fail(warn) region 里引用了没有执行器的 type,注册期一声不吭,运行时 NO_EXECUTOR
validateNodeConfigKeys engine.ts:2803(for (const node of flow.nodes)) hard-fail region 里的未声明 config 键完全不检查 —— #4277 要抓的 visibleIf 拼写错误,放进 loop body 就恢复成静默通过

三个都是 for (const node of flow.nodes) 这一行的问题,collectFlowGraphs(flow) 已经在 @objectstack/spec/automation 里,改动量各自都在个位数行。

为什么值得单开

不是没顺手做,是风险不对等,混在 #4381 里会让那个 PR 的 blast radius 说不清:

  • validateNodeTypes 是 warn,扩进 region 几乎零风险,可以直接做;
  • validateControlFlowvalidateNodeConfigKeys 都是 hard-fail,扩大遍历范围 = 可能新拒一批现在能注册的 flow。这是行为变更,不是 bugfix,需要先量一遍存量再决定。

建议的落地顺序

  1. 先量:拿 collectFlowGraphs 写一个只读脚本,跑 examples/app-showcaseexamples/app-crmexamples/app-todo 的全部 flow,统计三个校验器扩到 region 之后会新增多少 violation,分别是什么。没有这个数,后两步没法判断。
  2. validateNodeTypes 直接扩(warn,无风险)。
  3. validateNodeConfigKeys / validateControlFlow:如果第 1 步显示存量为 0,直接收紧;如果非 0,先把 region 部分降级成 warn 走一个版本,再提到 error —— 就是 feat(automation,formula): warn on undeclared flow-node config keys (#4045) #40593b — wire the flow executors to parse() their config, and tighten the undeclared-key warning into an error #4277 那条 warn-then-error 的阶梯。

复现

// 顶层:注册期 hard-fail(#4277)
{ id: 'scr', type: 'screen', config: { visibleIf: '…' } }

// 同一个节点放进 loop body:注册期静默通过
{ id: 'loop', type: 'loop', config: {
    collection: '{rows}',
    body: { nodes: [{ id: 'scr', type: 'screen', config: { visibleIf: '…' } }], edges: [] },
} }

validateControlFlow 的那一条同理 —— 把一个双入口(畸形)region 从顶层 loop 挪进另一个 loop 的 body,注册就不再拒了。

不在范围内

evaluateCondition 的 legacy {var} 路径上剩下的静默 false(无运算符分支、以及未解析的 {token} 被当字符串比)是另一类问题,和遍历无关,建议另开。

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions