Skip to content

script 的 config 契约要接入 #4277 的执行期 parse,先得有判别式(actionType)形态 #4343

Description

@os-zhuang

#4278 与 #4277 在同一天落地,两者交界处留下一个明确的后续,记在这里以免被当成「忘了接」。

现状

#4277(#4332)让 12 个「有契约」的内置节点在执行前 parse() 自己的
node.config(service-automation/builtin/parse-config.ts),并让 registerFlow()
拒绝 descriptor configSchema 未声明的键。

#4278(#4325)给另外三个 descriptor-schemaless 节点补上了执行器派生的契约
(spec/automation/schemaless-node-config.zod.ts:ScriptConfigSchema /
SubflowConfigSchema / DecisionConfigSchema),但刻意没有接入 parse,也没有发布
descriptor configSchema——两件事是同一个理由。

为什么不能直接接上去

它们的键集不等于契约。

  • script —— 合法键随 actionType 变:内置副作用分支读
    template / recipients / variables;函数分支读
    function / inputs / outputVariable;内联 script 是被识别但不执行的第三种。
    平铺 parse 只能取这些键的并集,于是
    { actionType: 'email', function: 'x' } 这种自相矛盾的配置照样通过——检了个寂寞;
    若反过来收紧成任一分支,就会拒绝另一半合法元数据。
  • decision —— 完全可以不带 conditions,只靠出边的 edge.condition 分支
    (这是遗留但合法的形态)。
  • subflow —— 这个其实最接近可接入:键集扁平,flowName 执行期必需,
    执行器已经用 refuseNode() 拒绝缺失的情况。

建议做法

  1. subflow 可以先接——低风险,把现有的 refuseNode 换成统一的 parse guard,
    与其它 12 个节点的失败形态一致(guard refusal,不走 fault 边)。
  2. script 需要先改造成判别式:按 actionType 拆成
    z.discriminatedUnion(或等价的 superRefine),分支分别声明各自的键。
    这同时会让「自相矛盾的配置」在作者时就被拒,而不是运行到一半才发现
    ——actionType: 'email' 却写了 function 是当前唯一还能悄悄通过的形态。
  3. decision 大概率维持现状:它的真实契约一半在边上,不在 config 里;
    接 parse 的收益仅限于「conditions[] 元素的类型」,可以等 2 做完再评估。

一个附带好处:script 一旦有了判别式形态,objectui 的
flow-node-config.spec-reconciliation.test.ts(objectui#3099)可以从「键集并集比对」
升级为「按 actionType 分支比对表单的 showWhen 分组」——那才是表单实际的形状,
也能挡住「某个分支少提供了一个字段」这类 #4278 没覆盖到的漂移。

Related

Activity

  1. os-zhuang commented on Aug 1, 2026

    @os-zhuang
    ContributorAuthor

    方向变更(操作者裁定):不再给 script 的多形态做判别式契约,改为激进收敛——script 收敛为纯函数调用节点(function 必需 + inputs + outputVariable),actionType/template/recipients/variables/script 五键按 ADR-0049 removal 路线打 retiredKey() 墓碑(处方:email → notify;slack → connector_action/http;内联脚本与函数名简写 → 注册函数),并注册 ADR-0087 D2 conversion。

    理由:四个分支里只有函数路径跑真实逻辑(email/slack 是 logger stub、内联 script 是 no-op、其余 actionType 是函数名别名),多形态是历史积累。收敛后契约天然平铺,script/subflow 直接接入 #4277 的执行期 parse——本 issue 的目标以更简单的形态达成;墓碑处方也正是经这条 parse 通道对存量 JSON 元数据发声(load 路径不 parse config)。decision 维持现状。

    本 session 接手实施,分支 claude/script-config-parse-contract-gy035e(objectstack + objectui 配套),PR 链接随后附上。


    Generated by Claude Code

  2. os-zhuang commented on Aug 1, 2026

    @os-zhuang
    ContributorAuthor

    PR 已开(均为 draft):

    两点执行中发现、与原计划不同的事实,记在这里:

    1. 墓碑对存量 JSON 元数据是听不见的,靠执行期 parse 也不是。load 路径从不 parse 节点 config(FlowNodeSchema.config 是 z.record(z.unknown())),而 registerFlow 会连退役 conversion 一起重放([P2] The spec contract stops at authored source — stored metadata is rehydrated unparsed, unconverted, and ungated #3903——sys_metadata 里的行没有作者可教)。所以存量流程走的是「注册时被 conversion 剥掉没人读的键 → 执行期因为没有 callable 被拒」,墓碑本身只对写 TypeScript 的作者和直接 parse 的调用者发声。真正的行为翻转是老的 email-stub 节点从「打一行日志报成功」变成「大声拒绝」。
    2. flow-node-script-config-aliases([P3] Six flow-node config aliases bypass readAliasedConfig — no warning, no ledger, no retirement path #3796)的 fixture 两侧都带 actionType: 'invoke_function',那个终态在 17 已不可达,fixture 已改写(conversion 本体未动)——与 wait 声明了超时契约但完全没有实现:onTimeout 零读取者,timeoutMs 被当成定时时长用 —— showcase 自己在依赖它 #4158 退役时踩到的是同一类交互。

    decision 按 issue 建议维持现状。


    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

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions