Skip to content

Flow 边检查器能产出缺 dialect 的 condition —— 写出的正是服务端自己 schema 会拒绝的形状 #3171

Description

@os-zhuang

批次 3 燃尽(objectui#3169 / objectstack#4115)时对读 FlowEdge 与 spec 同名导出发现,记录在案但有意未在该 PR 修——它改变写进元数据的内容,不该搭在一个符号派生的 PR 里。

缺陷

Flow 设计器的边检查器可以构造出只有裸表达式、不带 dialect 的 condition。而 ADR-0089 把 dialect 定为必填,服务端的 schema 会拒绝这种形状。

也就是说:设计器里能正常编辑、保存、看起来一切正常的一条边,提交到服务端会被拒——失败发生在离编辑现场最远的地方,而且用户没有任何线索知道是哪条边、缺了什么。

为什么值得单独修

这是"设计器写出的元数据与服务端契约不一致"这一类,和 objectstack#4115 的撞名符号是同一个根:objectui 侧有一份自己的形状理解,与 spec 各自演化。区别在于撞名符号的后果是类型信息失真,这个的后果是运行时被拒。

建议的修法

在边检查器构造 condition 的位置补上 dialect(取 ADR-0089 的规范默认值),并加一条闸门断言:任何由检查器产出的 condition 都能通过 spec 的 schema 解析。闸门比修复本身重要——没有它,下一次 spec 收紧同类字段会再次静默漂移。

同时建议排查是否还有别的"设计器产出 → spec 拒绝"的形状,方法可参考 objectstack#4115 里记过的线索:grep "as any).<field>" 能找出"运行时已适配、契约未跟上"的现场。

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮(分片:objectui 整仓)
    会话:session_01PRJtkgUAaVG11FsJQbvZWA
    分支:claude/issue-3171-flow-edge-condition-dialect
    Worktree:objectui-issue-3171

    分诊补充:按正文修法执行——补 dialect(取 ADR-0089 规范默认值)+ 闸门断言(检查器产出的任何 condition 都必须能通过 spec schema 解析)。正文明说闸门比修复本身重要,不得只改构造点而不加闸门。正文建议的 grep "as any)." 排查属调查范围:发现的同类「设计器产出 → spec 拒绝」形状按越界发现另立 issue,不在本 PR 顺手修。


    Generated by Claude Code

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    开发 agent 调查结论:未按正文实现,升级为待定。正文的前提经核实不成立,照修会在「不存在的 bug」上改掉写进元数据的形状。证据如下(均针对 objectui 当前锁定的 @objectstack/spec@17.0.0-rc.1)。

    1. 裸字符串 condition 不会被服务端拒绝 —— 它是 spec 明文祝福的创作输入

    FlowEdgeSchema.condition 是 ExpressionInputSchema.optional(),而 ExpressionInputSchema 是:

    z.union([ z.string().min(1).transform(source => ({ dialect: 'cel', source })), ExpressionSchema ])
    

    裸字符串在 parse 时被规范化成 { dialect: 'cel', source }。实测:

    FlowEdgeInspector patchEdge({condition: expr})   OK   -> {"dialect":"cel","source":"record.amount > 500"}
    mirrorBranchOntoEdge next.condition = expr       OK   -> {"dialect":"cel","source":"record.amount > 500"}
    

    服务端保存用的正是同一个 schema(spec/src/kernel/metadata-type-schemas.ts 的 flow: FlowSchema);objectui 自己的 clientValidation.ts 也拿它做草稿实时校验。ADR-0058 §正文把这条写死了:「ExpressionInputSchema → { dialect:'cel', source }」是创作层原语,约 50 个声明都走它,裸字符串就是创作期的正当写法。spec/src/automation/control-flow.zod.ts 的注释也明说「FlowEdgeSchema.condition 是 ExpressionInputSchema,所以裸字符串谓词会变成规范的 { dialect: 'cel', source } 信封」。

    2. 缺 dialect 的对象形状确实会被拒 —— 但没有任何构造点产出它

    { source: '...' } 无 dialect   FAIL -> condition: Invalid input
    

    这个形状只存在于 objectui 本地的读取型类型标注(FlowDesignerEdge.condition?: string | { source?: string }、FlowEdgeInspector 内的 FlowEdge)。逐个核过全部写入点,无一例外都写裸字符串:

    • previews/FlowCanvas.tsx:288 — newEdge.condition = expr
    • inspectors/FlowEdgeInspector.tsx:135 — patchEdge({ ..., condition: expr })
    • inspectors/FlowEdgeInspector.tsx:227 — patchEdge({ condition: v || undefined })
    • inspectors/flow-decision-edges.ts:105 — next.condition = expr

    批次 3 注释里那句「inspector can build a condition object the server's own FlowEdgeSchema would reject」,说的是类型允许,不是代码产出。类型宽松是读取侧的失真,不是运行时被拒。

    3. 顺带:ADR 引用有误

    正文说「ADR-0089 把 dialect 定为必填」。ADR-0089 是 visibleWhen 谓词命名统一,通篇不含 dialect。管表达式面的是 ADR-0058,而它的立场恰恰相反(见上)。

    4. 真正「设计器产出 → spec 拒绝」的形状是另一个,已另立 #3202

    applyDecisionBranches 新建边时不盖 id,而 FlowEdgeSchema.id 必填:

    applyDecisionBranches created edge   FAIL -> id: Invalid input: expected string, received undefined
    

    同仓 FlowCanvas.tsx 的三处建边都用 uniqueId('edge', …) 盖了 id,只有分支编辑器漏了。详见 #3202。

    需要维护者定的问题

    设计器该不该主动写规范信封 { dialect:'cel', source },还是继续写 spec 祝福的裸字符串? 这不是 bug 修复,是「objectui 往元数据里写什么形状」的政策选择,而且一旦选 A 就不能只改 flow 边一处 —— 否则 objectui 自己变成第三种方言,正是 objectstack#4115 / #3169 在燃的那个病。两个选项的完整权衡放在 dev 报告里,请与 #3172(同批次的几何三拼写)一并裁决。

    闸门断言本身无争议且应该有,但它今天落不了绿:诚实写成「检查器产出的边都能过 FlowEdgeSchema」就会被 #3202 的缺 id 拦下。所以闸门应随 #3202 一起落,而不是搭在本 issue 上。


    Generated by Claude Code

  4. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 裁决:关闭本条 —— 前提经核实不成立(选项 A)。开发 agent 的证伪结论我已独立复核,属实。

    复核证据(直接读 spec 源码,非转述)

    • packages/spec/src/automation/flow.zod.ts:308 —— condition: ExpressionInputSchema.optional();
    • packages/spec/src/shared/expression.zod.ts:92-95 —— ExpressionInputSchema = z.union([z.string().min(1).transform(source => ({ dialect: 'cel', source })), ExpressionSchema])。

    即裸字符串是被接受的,并在解析时自动归一化成 {dialect:'cel', source};服务端保存走的正是同一个 schema。因此「边检查器写出的 condition 会被服务端拒绝」这一前提不成立,按本条正文去补 dialect 属于为一个不存在的拒绝去改写入元数据的内容。

    补充两点:① 本条援引的 ADR-0089 是 visibleWhen 命名,与 dialect 无关;真正治理表达式作者化形态的是 ADR-0058,它明确把裸字符串定为作者化原语。② 被 spec 拒绝的那个「无 dialect 的对象」形状只存在于 objectui 的本地读类型(FlowDesignerEdge.condition?: string | { source?: string }),三个构造点写出的都是裸字符串——没有生产者会产出它。那个过宽的本地类型正是本条误判的来源。

    本条的真实价值:它找对了闸门,找错了形状

    正文说「闸门比修复本身重要」——这句是对的,而且被证实:同一代码路径上确实存在真的「设计器产出 → spec 拒绝」,是 applyDecisionBranches 新建的边不带必填的 id(FlowEdgeSchema.id: z.string(),无默认值,已实测 FAIL)。已另立 #3202,闸门断言随该条落地——写诚实的话它在这里根本绿不了(会被 #3202 的真缺陷绊倒),所以它属于那一条。

    顺带把本条暴露的根因也挂到 #3202:把 FlowDesignerEdge.condition 的本地读类型收紧到镜像 spec 的 ExpressionInput,让类型不能再描述一个 spec 会拒绝的形状(公共类型变更,按仓库版本政策发 minor)。留着它,下一个读者还会再推导出同一个错误结论——今天已经发生过一次。

    Closed as not planned(前提不成立,非「不修」)。真实缺陷见 #3202。


    Generated by Claude Code

  5. removed their assignment
    on Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions