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.

Hook 检查器把 { dialect, source } 信封形式的 condition 读成空字符串,提交时会清掉它 #3218

Closed

Description

@os-zhuang

做 #3216(模拟器读边守卫的信封形式)时越界发现,未在该 PR 里顺手改 —— 它是另一个界面(Hook 检查器)上的同族缺陷。

缺陷

packages/app-shell/src/views/metadata-admin/inspectors/HookDefaultInspector.tsx:256:

<ConditionBuilder
  label="Run only when (optional CEL)"
  value={typeof draft.condition === 'string' ? (draft.condition as string) : ''}
  onCommit={(v) => onPatch({ condition: v || undefined })}
  ...
/>

只认裸字符串,信封形式落到 ''。

而 @objectstack/spec 的 HookSchema.condition 用的正是 ExpressionInputSchema(和 FlowEdgeSchema.condition 同一个 pipe)。实测:

HookSchema.parse({ name:'h1', object:'lead', events:['beforeInsert'], condition:'amount > 10', handler:'x' }).condition
// => { dialect: 'cel', source: 'amount > 10' }

也就是说,作者写下 amount > 10、平台存成信封之后,再打开这个 Hook 的检查器:

  1. "Run only when (optional CEL)" 输入框显示为空 —— 作者据此以为这个 hook 没有条件、会无条件执行,而它其实有;
  2. 顺着这个错误认知的下一步就会丢数据:ConditionBuilder 的 emit 只编译当前 UI 里的行,所以作者在这个看起来空的构建器里加一条件并提交,onPatch 写下的是新条件替换掉原来那条(不是合并),而作者从头到尾没看见过原来那条。清空则提交 condition: undefined。

准确性说明(先核实过再写):onCommit 只在用户真的编辑时触发,ConditionBuilder 挂载/重渲染本身不会提交,所以"只打开看一眼"不会改动草稿 —— 丢失需要一次编辑动作,但那次编辑正是这个空输入框在诱导的。

为什么是真 bug 而不是设计

与 #3216 / #3202 同一族:一个过窄的本地读法与 spec 不一致。仓内其它读同类字段的地方都两种拼写都认(previews/flow-canvas-layout.ts 的 conditionText、inspectors/expression-validate.ts 的 validateExpressionClient、inspectors/flow-ref-check.ts 的 findUnknownRefs),漏掉分支的只有这一处。#3216 已经把流程边守卫的两处收敛掉了,这是同族里另一个界面上的剩余项。

建议修法

读侧走仓内既有的那一个读法(conditionText,或 #3216 之后沉淀下来的共享读法),不要再写第四份 typeof c === 'string'。

写侧要一并想清楚,这是本条真正需要决策的地方,不宜由实现者猜:onCommit 目前提交裸字符串。可选项大致是

  • A:继续提交裸字符串(spec 的 pipe 会在 parse 时规范化成信封),读侧只管兼容两种;
  • B:提交时保留原信封的 dialect / meta,只替换 source,这样作者手工写的 meta(ADR-0089 的 rationale / generatedBy)不会因为改一次条件就丢掉。

倾向 B,但需要确认 meta 在这条路径上是否真的会被保留到保存。另外本 issue 只核实了 Hook 这一处;ConditionBuilder 的其它调用点是否有同样的 typeof === 'string' 读法,需要在动手时一并普查。

验证建议

一个 hook 的 condition 存成 { dialect: 'cel', source: 'amount > 10' },打开 Hook 检查器:期望输入框显示 amount > 10;当前显示为空,随后任一次编辑提交都会用新值替换掉那条不可见的原守卫。

相关

Activity

  1. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    裁决:B,并且 needs-user-decision 撤掉 —— 读过 spec 之后这不再是个偏好题

    本单把写侧列为「需要决策、不宜由实现者猜」是对的。但决策所依赖的那个事实(「确认 meta 在这条路径上是否真的会被保留」)是可查的,查完之后 A 不是「另一个合理选项」,而是会静默损坏数据。

    packages/spec/src/shared/expression.zod.ts:42-83:

    export const ExpressionDialect = z.enum(['cel', 'cron', 'template']);   // ← 不是只有 cel
    
    export const ExpressionSchema = z.object({
      dialect: ExpressionDialect,
      source: z.string().min(1).optional(),
      ast: z.unknown().optional(),        // Phase 2 (M9.2+): ast becomes required in build output
      meta: ExpressionMetaSchema.optional(),   // rationale / generatedBy
    }).refine(e => e.source !== undefined || e.ast !== undefined, …);

    A 会毁掉三样东西,meta 是其中最不重要的一样

    选项 A 是「提交裸字符串,让 spec 的 pipe 去规范化」。但那个 pipe 是
    z.string().transform(source => ({ dialect: 'cel', source })) —— 它把 dialect 硬编码成 cel。于是:

    1. dialect 被改写。一条存成 { dialect: 'template', source: … } 或 { dialect: 'cron', … } 的 hook 条件,作者在这个检查器里编辑一次,就变成 cel。这不是丢失附属信息,是把守卫换成另一种语言去求值——本单描述的「作者从没见过原来那条」在这里升级为「作者以为自己只改了表达式,实际上换了求值引擎」;
    2. ast 被丢弃。按文件里写明的 Phase 2 路线,ast 会成为构建产物里的权威表示、source 退为 round-trip/debug。写侧只发裸字符串的路径在那之后是直接扔掉权威表示;
    3. meta(rationale / generatedBy)被丢弃——注释写明它的用途正是「AI 生成的产物,让审计/解释工具有东西可渲染」。本仓的 AI 作者路线上,这恰恰是最常被填、也最没人会手工补回来的一份。

    三条都是静默的:改完能存、能跑、schema 校验通过。这正是 ADR-0049 / ADR-0078 那一族的失效形态。

    所以 B,但要补一条本单没写的:ast 必须丢,不能跟着保留

    「保留原信封的 dialect / meta,只替换 source」的字面执行会连 ast 一起留下,而那个 ast 是从旧 source 编译来的。留着它,在 Phase 2 之后就是:引擎按旧 AST 求值,界面显示新 source——比现在这个 bug 更难查。

    准确的写侧规则:

    键 source 被编辑时
    dialect 保留原值(没有原信封时才落 'cel')
    meta 保留
    source 替换为新值
    ast 丢弃(它是旧 source 的派生物;objectstack compile 会重新填)

    丢弃 ast 后 refine 仍满足(source 在)。清空条件时照旧提交 condition: undefined。

    四轴对照

    • 防 AI 犯错:AI 作者产出的正是带 meta.generatedBy 的信封;A 让「人改一个字」等于「抹掉这条守卫的来历」。
    • 平台长远合理性:ast 的 Phase 2 路线已经写在 spec 里,写侧现在就不该建立一条会丢弃它的路径。
    • 实际业务需求:cron / template 的 hook 条件今天就可表达,dialect 改写是当下就能踩的。
    • 不扩边界:B 不新增任何概念,只是不破坏已存在的键。

    施工要求

    维护者若不同意,推翻即可——但请注意 A 的代价是 dialect 改写,不只是丢 meta。


    Generated by Claude Code

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

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 6 轮(分片:objectui 整仓)
    会话:session_01PRJtkgUAaVG11FsJQbvZWA
    分支:claude/issue-3218-hook-inspector-expression-envelope
    Worktree:objectui-issue-3218

    写侧按上一条评论的裁决 B(含 ast 必须丢弃那一条)执行,needs-user-decision 已撤。

    同轮并行:#3221(packages/fields)、#3223(packages/layout)。本条在 packages/app-shell/src/views/metadata-admin/,三者文件宇宙不相交。只读引用 previews/flow-canvas-layout.ts 的 conditionText 可以,不要改它(#3216 刚把它定为唯一读法)。


    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