Skip to content

Design: should FlowNodeSchema parse its ADR-0031 regions, instead of a post-parse pass callers must remember to run? #4415

Description

@os-zhuang

设计讨论,不是 bug —— 现状能工作,问的是它能不能不依赖调用方的记性。接 #4347 / #4381 / #4399 / #4401。

现状

FlowSchema.parse 归一化 flow 自己的 nodes[] / edges[],但够不到 region,因为 region 住在 FlowNodeSchema.config 里 —— 一个刻意开放的 z.record。#4381 用一道后置 pass 补上:

const flowShell = FlowSchema.parse(converted);
validateControlFlow(flowShell);
const parsed = normalizeControlFlowRegions(flowShell);   // ← 必须记得调

问题

「parse 完拿到的就该是规范形状」是 Zod-First(PD #1)的默认预期。现在多了一条不成文规则:拿到 FlowParsed 之后还得再调一个函数,否则嵌套 region 里的边条件还是裸字符串、节点还没过 .strict()。

今天只有一个消费方(registerFlow),所以没出事。但这正是 #4347 那一族缺陷的生成条件 —— 一个新消费方(Studio 发布路径、未来的 MCP 工具、某个批量校验脚本)拿到 FlowParsed 就直接用,就又是一次「看起来解析过了、其实只解析了一半」。

为什么 #4381 当时没做

评估过、刻意没做,理由是 blast radius:

  • 给 FlowNodeSchema 挂 .transform() 会把它从 ZodObject 变成 ZodPipe,而它被 form 生成、z.toJSONSchema、以及 FlowRegionSchema / ParallelBranchSchema 反过来引用;
  • lazy-schema.ts 的注释里已经记着 .strict().transform(…) pipe 曾经把 toJSONSchema 的 seen 表打崩过(ADR-0089 D3a 时 FormFieldSchema / PageComponentSchema 踩过),现在靠一个 _zod facade 兜着;
  • region 是自递归的(FlowNodeSchema → FlowRegionSchema → FlowNodeSchema),transform 里再套一层解析要小心构造期递归。

把这些混进一个 bugfix PR 里,会让那个 PR 的风险说不清 —— 所以当时选了后置 pass,并在函数注释里写明了这笔债。

可选方向(未定)

  1. FlowNodeSchema 自解析 region。 最符合 PD Add metamodel interfaces for ObjectQL/ObjectUI contract #1,一次解决所有消费方。代价见上,需要先确认 toJSONSchema / form 生成不受影响。
  2. FlowSchema 层挂 transform。 比 1 影响面小(FlowNodeSchema 保持 ZodObject),但嵌套递归得自己写,且绕过 FlowSchema 直接 FlowNodeSchema.parse 的调用方仍然漏。
  3. 维持现状 + 加护栏。 比如让 collectFlowGraphs / runRegion 在遇到未归一化的 region 时 warn,把「忘了调」变成有声音的。成本最低,但没有消除那条不成文规则。

倾向 1,前提是先量清楚 toJSONSchema 的影响 —— 不该在没有这个数之前动手,这和 #4389 「先量再改」是同一个道理。

相关

#4336 的建议修复 #3(「给 node config 上类型」)是邻近但不同的一件事:那条针对表达式归一化够不到 config.condition,这条针对 region 子结构。两者如果都做,大概率是同一个机制的两次应用。

Activity

  1. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    裁决(PM 代决,维护者可否决):「先量再改」,倾向方向 1 ——

    第一步派测量任务:量清 FlowNodeSchema 加 .transform()(ZodObject→ZodPipe)后对 z.toJSONSchema、form 生成、lazy-schema seen 表的实际影响(ADR-0089 D3a 有前科,.strict().transform pipe 曾打崩 toJSONSchema)。

    注意:近期有 region-slots changeset 活动,测量以最新 main 为准;与 #4336(node config 上类型)大概率是同一机制两次应用,执行时互相知会。已入队待派(首派 = 测量)。


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    分诊级别建议:本单是「Design: should…」形设计卡(FlowNodeSchema 是否内建解析 ADR-0031 regions),属公开契约形状取向,派发前需要维护者拍板——建议摘 pm:queue 换 needs-user-decision 进决策箱。交分诊座位定夺。背景:维护者 2026-08-06 拍板 spec 车道缩盘方案,见 #5837。


    Generated by Claude Code

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    标签落实(维护者 2026-08-06 指示,session_01LeEfA7CFwbJb7JJmXm2KM3):按 08-06 降级建议执行 pm:queue → needs-user-decision——「Design: should…」形设计卡(FlowNodeSchema 是否内建解析 ADR-0031 regions),进决策箱等拍板。


    Generated by Claude Code

  4. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Maintainer ruling recorded (2026-08-07, in chat — 「按建议」 approving the premise-gated plan from the release-board audit):

    Proceed measurement-first, one dispatch, two pre-ruled branches:

    • Premise to verify BEFORE any implementation: the ZodPipe shape produced by .transform() on the self-recursive FlowNodeSchema can be digested by the existing generators — the toJSONSchema walker and the lazy-schema path, i.e. exactly the shape that crashed before (ADR-0089 D3a). Verify by measurement (a minimal spike against the real generators), not by reading.
    • Premise holds → implement direction 1: FlowNodeSchema parses its ADR-0031 regions itself; the post-parse pass retires.
    • Premise falsified → ⛔ do not force it; implement directions 2+3 (FlowSchema-level transform + warn guardrails) and record the measurement evidence in the PR.

    Neither branch returns to the maintainer — both are pre-ruled. Dispatching now.


    Generated by Claude Code

  5. self-assigned this
    on Aug 7, 2026
  6. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Claim: PM session (maintainer-directed dispatch, 2026-08-07 「按建议」 — premise-gated plan recorded in the ruling comment above)
    Session: session_01BickTBKm2JYSNnrtPT8ysa
    Branch: claude/issue-4415-flownode-regions-parse
    Worktree: objectstack-issue-4415
    Domain: domain:spec (maintainer-directed from chat; spec seat may sequence/veto — no collision intended with in-flight spec work)
    File surface: packages/spec/src/** flow schema modules (flow.zod.ts family) + the ADR-0031 regions post-parse pass module + spec generators ONLY if the premise measurement requires adapting them; spec build regenerates its own baselines (os-regen discipline applies)
    Container judgment: M (spec build + test verification) → mode:subagent, shared container
    Serial constraints cleared: no open PR touches the flow schema family per the open-PR list; duplicate-claim CI gate backstops. Measurement-first per the ruling — premise falsified ⇒ branch 2+3, never forced.


    Generated by Claude Code

  7. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Review: ACCEPT → PR #6333, flipped ready, auto-merge enabled. (PM session claude/rc4-release-tasks-analysis-2d8euw)

    Verified independently, not from the dev report alone:

    • Premise gate honored — the PR body documents the measurement-first spike against the real generators (toJSONSchema walker under OS_EAGER_SCHEMAS=1, form-generation gates, lazy-schema seen-table via gen:openapi + reused-Proxy probe) before implementation, exactly as the 2026-08-07 ruling required. Premise held → direction 1 landed; the post-parse pass retires with its call site.
    • Changeset level checked by hand (the one thing CI can't judge): @objectstack/spec: minor + @objectstack/service-automation: patch — correct for the v17 train; does not derail the version PR to 18.0.0-rc.0. Migration section covers both the normalizeControlFlowRegions removal and the ZodObject→ZodPipe shape change (.def.in guidance).
    • CI converged, read from check runs: 24/24 complete, 23 success + 1 skipped (Console Pin Gate — normal when .objectui-sha untouched). Zero red; Check Changeset genuinely green (real changeset ships, no skip label).
    • Reverse verification ran in the honest direction: both reverts predicted RED before running, both confirmed, including the wholesale replacement of the one assertion that pinned the old defect by design.

    Out-of-scope finding correctly filed unassigned as #6267 (pre-existing TDZ, reproduced on pristine main).

    Note for the spec-surface lane: once this merges, #6094 unblocks (it was premise-gated on this landing).


    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