Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Flow 分支编辑器提交时新建的边没有 id —— 这才是「设计器产出、spec 拒绝」的真实形状 #3202

Closed

Description

@os-zhuang

调查 #3171 时发现并核实,越界发现另立(未在 #3171 的 PR 里顺手修)。

缺陷

packages/app-shell/src/views/metadata-admin/inspectors/flow-decision-edges.ts 的 applyDecisionBranches,在「某分支填了 Target、但没有可复用的出边」时会新建一条边:

else created.push(mirrorBranchOntoEdge({ source: decisionId, target }, b));

新建的边不带 id。而 @objectstack/spec 的 FlowEdgeSchema 里 id: z.string() 是必填(不是 .optional())。

实测(objectui 当前锁定的 @objectstack/spec@17.0.0-rc.1):

branch-created edge (no id)         :: FAIL [{"p":["id"],"m":"Invalid input: expected string, received undefined"}]
branch-created default edge (no id) :: FAIL [{"p":["id"],"m":"Invalid input: expected string, received undefined"}]
canvas-created edge (has id)        :: OK

服务端保存走的正是同一个 schema —— packages/spec/src/kernel/metadata-type-schemas.ts 里 flow: FlowSchema,FlowSchema.edges 是 z.array(FlowEdgeSchema)。

为什么是真 bug 而不是设计

同仓的另一个建边现场 —— previews/FlowCanvas.tsx 的 addNode / insertOnEdge / 一键 revise 回路 —— 都用 uniqueId('edge', 已有 id 列表) 盖了 id。只有分支编辑器这一条路径漏了。也就是说仓库自己的约定是「提交出来的边必须有 id」,这里是个遗漏,不是有意的宽松。

flow-canvas-layout.ts 里 FlowDesignerEdge 把 id 标成可选,注释写明理由是「正在画的边在提交前没有 id」—— 那是编辑中的瞬态。applyDecisionBranches 的输出是已提交状态(直接进 onPatch → draft → 保存),不该沿用瞬态的宽松。

影响

clientValidation.ts 已经拿 FlowSchema 做草稿实时校验,所以作者会看到一条 edges.N.id: Invalid input: expected string, received undefined。后果不是静默,而是:设计器自己造出一条它自己标红的边,而作者没做错任何事,也没有任何 UI 能补这个 id。不修的话保存会被服务端 422 挡下。

建议修法

applyDecisionBranches 新建边时按仓内既有约定盖 id:uniqueId('edge', 现有边 id)(inspectors/_shared.tsx 已导出)。注意同一次提交可能新建多条边,id 要在本次累积的集合上取,避免撞号。

并补一条闸门单测:applyDecisionBranches / syncDecisionEdgesByOrder 产出的每一条边都要能过 FlowEdgeSchema.safeParse —— 这条闸门正是 #3171 正文要求的那条,只是它真正拦住的是这个形状。

相关

Activity

  1. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 分诊(第 2 轮结转):入队,下一轮派发(现在不派——它落在 app-shell,与在途的 #3174 同包,批内文件互斥要求错开)。

    裁决(PM 可决,依据仓内既有约定,维护者可否决),本条 PR 交付三件:

    1. 修 id:applyDecisionBranches 新建边按仓内既有约定盖 uniqueId('edge', 现有边 id);注意同一次提交可能新建多条,id 要在本次累积集合上取,避免撞号。
    2. 闸门(Flow 边检查器能产出缺 dialect 的 condition —— 写出的正是服务端自己 schema 会拒绝的形状 #3171 正文要求、真正拦得住东西的那条):applyDecisionBranches / syncDecisionEdgesByOrder 产出的每一条边都必须通过 FlowEdgeSchema.safeParse。
    3. 收紧本地读类型(Flow 边检查器能产出缺 dialect 的 condition —— 写出的正是服务端自己 schema 会拒绝的形状 #3171 的根因,随本条一起清):FlowDesignerEdge.condition 从 string | { source?: string } 收紧为镜像 spec 的 ExpressionInput(string | { dialect: ExpressionDialect; source?: string; ast?: unknown }),FlowEdgeInspector 内同形状一并处理。这是公共类型变更,按版本政策发 minor,changeset 写明。理由:那个过宽的类型已经导致一次错误的缺陷判定(Flow 边检查器能产出缺 dialect 的 condition —— 写出的正是服务端自己 schema 会拒绝的形状 #3171),类型不能描述 spec 会拒绝的形状,才不会有下一次。

    不在本条范围:#3172(节点几何 ui:{x,y} vs position)是另一条,不要顺手改。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 3 轮(分片:objectui 整仓)
    会话:session_01PRJtkgUAaVG11FsJQbvZWA
    分支:claude/issue-3202-branch-edge-missing-id
    Worktree:objectui-issue-3202

    裁决见上方第 2 轮结转评论(三件交付:补 id / 闸门 / 收紧本地读类型)。补充一条施工提醒:验证范围必须覆盖 packages/core——本轮已有一次因只跑改动所在包而漏掉跨包门禁的返工(#3174),那道棘轮就住在 core。


    Generated by Claude Code

  4. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    验收:ACCEPT → PR #3215(CI 15 项全绿,进入合并)。

    三件交付齐活:① applyDecisionBranches 新建边用 uniqueId('edge', …) 盖 id,取号集合 = flow 已有边 id + 本次提交中先前铸出的 id(多分支一次 apply 不撞号,单列了用例);② 闸门覆盖 create/update/retarget/detach/by-order 五类场景的每一条边过 FlowEdgeSchema.safeParse,并另有一条把失败形状本身钉住——反证有效:把补 id 那行改回原样后 5 个测试立刻红;③ 读类型收紧,FlowEdgeInspector 与 FlowPreview(编译器逼出来的第三份逐字复制,已删复制改为 type FlowEdge = FlowDesignerEdge)一并处理。

    两处偏离我的裁决,均更正确,予以采纳:

    1. 我给的收紧形状 { dialect; source?; ast? } 漏了 spec 对象分支上可选的 meta,照抄会让本地类型比 spec 更窄——把"类型撒谎"换了个方向。改为直接 import type { ExpressionInput },与仓内 check:spec-symbol-derivation「派生而非重述」一致,且镜像不会随 spec 演进过期。
    2. 类型收紧只在编译期可观测(inspector 只写裸字符串),所以断言文件被列进 tsconfig.typetests.json——否则就是又一个"声明了但没执行"。实测反写断言会报 TS2344,证明它真的被编译。

    uniqueId 挪到无 React 模块(_shared 继续 re-export,调用点不变)属必要的连带改动:纯模块单测的模块加载 7.4s → 63ms,且避免铸第二个 id 生成器。

    越界发现已立案:#3216(flow 模拟器的两个读边守卫只认裸字符串,把规范化后的 {dialect, source} 信封当成"没有条件"——决策出边在模拟时间线上报 Branch has no condition. 并被跳过,与引擎和画布不一致;SimEdge.condition 是本条收紧掉的那份过宽形状的最后一份复制)。这条与本轮主题一脉相承,建议优先:它是同一个过宽类型在求值路径上的实际后果,不是类型问题。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions