来源
#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):
引擎只在单 id 分支赋值 previous。 packages/objectql/src/engine.ts:4888 的 if (priorRecord) hookContext.previous = ...,而 priorRecord 仅在 if (hookContext.input.id) 分支内被 fetch;批量分支始终为 null。
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。
flow trigger 读的正是这两个。 packages/triggers/trigger-record-change/src/record-change-trigger.ts:291-293:
const previous = (ctx.previous ...) ?? ((ctx as ...).__previous ?? undefined);
→ 批量写上 previous 恒为 undefined。
record 在批量写上也不是行。 引擎批量分支 result = await driver.updateMany(...) 返回的是受影响行数 (number)。buildContext 的 after && typeof after === 'object' 因此为 false,record 退化成 inputDoc —— 即本次写入的裸 payload ,而不是任何一行的状态。
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. 按行触发 —— flow trigger 在批量写上取回匹配行集,按行构造上下文。语义最正确,与 批量写上 hook 的 previous:B1 已拍板(fail loud 无例外,并入 #4775);本条转为「批量写 hook 按行触发」设计卡孵化点 #4800 的 A 方向同源(两者应一起定,否则又是两套规则);代价是批量写多一次 read + N 次 flow 评估。
B. 响亮拒绝 —— 起始条件引用 previous(或引用 payload 未设置的字段)时,批量写直接失败,文案与 hook 的 condition 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 的 B1 同源。一致性最好,但把 flow 的声明错误变成写入失败,爆炸半径与 hook 的 condition 求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 承担过的是同一类。
C. 显式跳过 + 可观测 —— 保持不触发,但按 error 级别上报并计数,至少让缺失可见。成本最低,但保留了「声明了却没兑现」。
倾向 A ,并与 #4800 一起定方向;若 A 成本过高,B 比 C 更符合本仓已经选定的「declared = enforced」路线。
关联
来源
#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):previous。packages/objectql/src/engine.ts:4888的if (priorRecord) hookContext.previous = ...,而priorRecord仅在if (hookContext.input.id)分支内被 fetch;批量分支始终为null。__previous兜底也不覆盖批量。packages/plugins/plugin-audit/src/audit-writers.ts:425:if (!id) return; // bulk update/delete — too costly to snapshot every row here。packages/triggers/trigger-record-change/src/record-change-trigger.ts:291-293:const previous = (ctx.previous ...) ?? ((ctx as ...).__previous ?? undefined);→ 批量写上
previous恒为undefined。record在批量写上也不是行。 引擎批量分支result = await driver.updateMany(...)返回的是受影响行数(number)。buildContext的after && typeof after === 'object'因此为 false,record退化成inputDoc—— 即本次写入的裸 payload,而不是任何一行的状态。为什么这条要单独立单
这不是一个理论缺口 —— 官方文档与 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 这一格依然是静默的。同一个平台对同一个物理限制给了两种相反的处理。可选方向(未拍板,留给维护者)
previous(或引用 payload 未设置的字段)时,批量写直接失败,文案与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 的 B1 同源。一致性最好,但把 flow 的声明错误变成写入失败,爆炸半径与 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 承担过的是同一类。倾向 A,并与 #4800 一起定方向;若 A 成本过高,B 比 C 更符合本仓已经选定的「declared = enforced」路线。
关联
condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775(已实现,PR feat(objectql)!: hook 的 condition 求不出值时 fail loud —— 抛错并中断该次操作 (#4775) #4861)—— 其 B1 错误文案刻意不指向 flow trigger,依据即本条skills/objectstack-formula的绑定作用域表要补一行:#4775 之后「condition 求不出值 = 该次写入失败」 #4814(SKILL.md §5 绑定作用域表)