Skip to content

record-change flow trigger 在 predicate 批量写上:previous 不绑定、record 只是裸 payload —— 官方文档与 showcase 正在教的 transition 起始条件在批量写上不成立 #4862

Description

@os-zhuang

来源

#4775(hook condition fail loud)落地过程中,为核实 B1 错误文案能不能把「改用 record-change flow trigger」写成出路而做的实测。核实结论是不能,#4775 因此没有在错误信息里指这条路(维护者定的硬防线:不允许在修 declared ≠ enforced 的 PR 里于错误信息中再造一个同类缺陷)。核实过程本身暴露出这条独立缺陷,按 PD #10 单独记录。

与 #4800 相邻但不是同一件事:#4800 是「批量写上 hook 要不要按行触发」的设计卡;本条是 record-change flow trigger 这一层,在批量写上交付给 flow 起始条件的上下文本身就是残缺的。

事实(实测,非推断)

对一次 predicate 批量写(multi: true,无 id):

  1. 引擎只在单 id 分支赋值 previous。 packages/objectql/src/engine.ts:4888 的 if (priorRecord) hookContext.previous = ...,而 priorRecord 仅在 if (hookContext.input.id) 分支内被 fetch;批量分支始终为 null。
  2. plugin-audit 的 __previous 兜底也不覆盖批量。 packages/plugins/plugin-audit/src/audit-writers.ts:425:if (!id) return; // bulk update/delete — too costly to snapshot every row here。
  3. flow trigger 读的正是这两个。 packages/triggers/trigger-record-change/src/record-change-trigger.ts:291-293:
    const previous = (ctx.previous ...) ?? ((ctx as ...).__previous ?? undefined);
    → 批量写上 previous 恒为 undefined。
  4. record 在批量写上也不是行。 引擎批量分支 result = await driver.updateMany(...) 返回的是受影响行数(number)。buildContext 的 after && typeof after === 'object' 因此为 false,record 退化成 inputDoc —— 即本次写入的裸 payload,而不是任何一行的状态。
  5. hook 只触发一次,所以即使匹配了 N 行,flow 也只被评估一次。

为什么这条要单独立单

这不是一个理论缺口 —— 官方文档与 showcase 正在教的写法,恰好就落在这一格上:

  • content/docs/data-modeling/formulas.mdx 与 skills/objectstack-formula/SKILL.md §5 教的 transition 写法是 previous.x != ... && record.x == ...;
  • examples/app-showcase/src/automation/flows/index.ts 里至少 10 条 flow 的起始条件是 status == "done" && previous.status != "done" 这个形状(第 39/317/424/671/780/896/976/1064/1148/1463 行等)。

对一次批量更新(例如把一批 task 一次性置为 done),这些 flow 的起始条件面对的是「previous 未绑定 + record 只有 payload」:该触发的自动化要么不触发,要么按一条并不存在的『记录』触发一次,而且没有任何东西会说出来。审计/通知类 flow 的缺失是不可见的缺失。

与 #4775 的对照值得注意:hook condition 在这一格现在会响亮地失败(B1 专门诊断);而 flow trigger 这一格依然是静默的。同一个平台对同一个物理限制给了两种相反的处理。

可选方向(未拍板,留给维护者)

倾向 A,并与 #4800 一起定方向;若 A 成本过高,B 比 C 更符合本仓已经选定的「declared = enforced」路线。

关联

Activity

  1. claude commented on Aug 4, 2026

    @claude
    Contributor

    分诊:过升级门槛,挂 needs-user-decision 上交维护者 —— 与 #4800 联合拍板

    正文的 A/B/C 三方向是真正的产品语义分歧,且 A(按行触发)与 #4800(批量写上 hook 是否按行)同源 —— 分开定必然产出两套规则。按决策分析两轴补充:

    • 项目长远合理性:A 是唯一让「官方文档与 showcase 正在教的 transition 条件」在所有写路径上语义成立的方向;B 一致但把声明错误变成写入失败(hook 的 condition 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 已为同类爆炸半径付过学费);C 保留 declared ≠ enforced,与本仓路线相悖。
    • 防 AI 写元数据犯错:A 落地后声明即兑现,最优;B 次之(契约收紧、错误响亮,AI 写出引用 previous 的 flow 会在第一次批量写上被立刻打回);C 最差 —— 静默跳过恰是 AI 批量犯错被掩盖的温床,即便加了 error 日志也只是把坑标了个牌子。

    建议:A,与 #4800 同一次拍板;若 A 的成本(批量写多一次 read + N 次评估)不可接受,取 B,不取 C。

    拍板前本车道不派发;showcase 那 10 条受影响 flow 在拍板后随实现单一并处理。


    Generated by Claude Code

  2. claude commented on Aug 4, 2026

    @claude
    Contributor

    维护者已裁(2026-08-04):取 A(按行触发),与 #4800 同一契约、同一实现单交付

    trigger 侧不单独动 —— 修复随「批量写按行语义」实现单(见 #4800 的裁定评论与新立实现单)一并落地;showcase 受影响的 10 条 flow 届时一并验证。摘 needs-user-decision,挂 Blocked-by 到实现单,拍板已毕。


    Generated by Claude Code

  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    PM 分诊(p0 #5088 集中处理的同一族,第 1 轮):标记 needs-user-decision,建议与 #4800 同批拍板;本条不阻塞 #5088。

    1. 与 #5088 的边界

    今天开的 p0 #5088(updateMany 对不存在的 id 报 INTERNAL_ERROR 而非 RECORD_NOT_FOUND)与本条同属「批量写上下文残缺」这一族,但不是同一条路径:

    因此 #5088 按「恢复既有不变量」直接派发,派发词明确禁止改动 predicate 批量分支的 hook / flow 语义 —— 本条的决定权范围不受影响。

    顺带一条对本条有用的实测:#5088 排查时确认 protocol.ts 上 #4435 的「按行诚实」只落在 5 个写面里的 2 个(updateData、deleteManyData),updateManyData 与 batchData 的 update/delete 分支都还缺。与本条是同一个形状的又一例 —— 一个修复只落在提出者当时站着的那扇门上,其余同族入口原样留着。

    2. 为什么标 needs-user-decision

    正文明写「可选方向(未拍板,留给维护者)」,且 A/B/C 已按两条轴分析完整,PM 不再补。要拍板的是 flow 起始条件在批量写上的契约形状,属升级门槛第一条(产品语义 / 公开契约),PM 不代答。

    需要维护者知道的两点权重输入:

    1. 这不是理论缺口 —— 文档与 showcase 正在教的写法恰好落在这一格:content/docs/data-modeling/formulas.mdx、skills/objectstack-formula/SKILL.md §5 教的是 previous.x != … && record.x == …,而 examples/app-showcase 里约 10 条 flow 的起始条件就是这个形状。一次批量置 done 时,这些 flow 要么不触发、要么按一条并不存在的「记录」触发一次,且没有任何东西会说出来。审计 / 通知类 flow 的缺失是不可见的缺失。
    2. 同一平台对同一物理限制给了两种相反处理:hook condition 侧现在响亮失败(hook 的 condition 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 B1),flow trigger 侧依然静默。选 C(显式跳过 + 可观测)会把这个不一致固化下来,选 B 会统一到「响亮」,选 A 才是消除限制本身。

    3. 与 #4800 必须同批

    #4800 的 A 与本条的 A 同源(批量写按行触发)。分开拍板必然产出两套规则,这正是本条正文点名要避免的。已在 #4800 同步标记并交叉说明。


    Generated by Claude Code

  4. added a commit that references this issue on Sep 4, 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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions