来源
#4784(PR #4799)把 hook condition 的 CEL 作用域扩成 record + previous。单记录 update 那条路已经完整:previous 搭 engine.update 既有的那一次 prior 取数,总全、不回灌、insert 上不绑定(与 validation 侧逐字一致)。
predicate(multi: true)批量更新这条路没有答案,#4784 的拍板意见里也没有 —— 它恰恰是 issue 原文点名「要先定『拿不到时 previous 是什么』」的那一格。
现状
engine.ts 的批量分支:
对照:validation 侧在批量上是有答案的 —— rulesNeedRows 逐行取 prior,evaluateValidationRules 每行调一次(#3106)。hook 没有这个「每行一次」的形状,所以不能照抄。
为什么现在值得单独定夺
#4775 已拍板方案 B:条件求不出值 → 抛错中断该次操作。落地之后,只要某个对象上有一条引用 previous 的 hook 条件,该对象的每一次批量更新都会写入失败 —— 失败原因还指向一个跟这次写入无关的 hook。#4784 修好的是「照文档写的 hook 不再静默失效」,这一格是它剩下的、会在 #4775 之后变成事故的部分。
派发 #4784 时给的成本提示是「引用 previous 会让 bulk predicate update 逐行取数」。逐行取数本身不难(复用 rulesNeedRows 那条路),难的是取回来之后绑给谁 —— 一次 hook 调用配 N 行前态。所以 #4799 的文档写的是实际行为(bulk 上不绑定),没有写那句提示,以免又一次 declared ≠ delivered。
可选方案(两条轴)
A. 批量写上按行触发带 previous 条件的 hook
引用 previous 的条件 → 逐行取 prior → 该 hook 按行求值、按行触发,ctx.previous = 该行。
B. 明确批量上 previous 不可用,并给一条专门的诊断
保持不绑定,但在 #4775 的 fail loud 里为这一格出一条点名的错误(「hook 条件引用了 previous,但这是一次 predicate 批量更新,没有单一前置记录;请改用单记录写入,或把过渡语义放到 record-change flow trigger」),而不是默认的 No such key: previous。
- 长远合理性:中 —— 不动 hook 契约,但把「过渡语义在批量写上不可表达」写死成平台性质。
- 防 AI 写错:中上 —— 错误响亮且指路,但 AI 仍会先写出错的形状再被拒。
- 代价:批量写入仍然会失败(只是失败信息有用了)。等于把成本转嫁给作者。
C. 批量写上跳过引用 previous 的条件(视作不适用,不阻断写入)
建议
A,若一次做不动则先上 B(它与 #4775 同批即可完成,成本低),把 A 留给「after 型 hook 在批量写上是否统一按行触发」那次更大的定夺。理由沿两条轴:长远上 A 才是让 hook 与 validation 对同一件事给同一个答案(#3106 已经在 validation 侧付过这笔设计成本);防 AI 写错上,A 让作者只需要记一套过渡写法,而 B 要求他们记住一条例外 —— 但 B 至少让例外响亮,远好过今天的静默。
关联
来源
#4784(PR #4799)把 hook
condition的 CEL 作用域扩成record+previous。单记录 update 那条路已经完整:previous搭engine.update既有的那一次 prior 取数,总全、不回灌、insert 上不绑定(与 validation 侧逐字一致)。predicate(
multi: true)批量更新这条路没有答案,#4784 的拍板意见里也没有 —— 它恰恰是 issue 原文点名「要先定『拿不到时previous是什么』」的那一格。现状
engine.ts的批量分支:triggerHooks('afterUpdate', hookContext)),hookContext.previous从不赋值(if (priorRecord)只在单 id 分支成立);condition的 CEL 作用域补上previous—— 过渡语义可写,与 validation 谓词对齐 (#4784) #4799 之后,批量更新上previous保持不绑定 —— 一条previous.done != true && record.done == true的条件不可求值,今天的表现是 warn + 当作 false(hook 跳过)。对照:validation 侧在批量上是有答案的 ——
rulesNeedRows逐行取 prior,evaluateValidationRules每行调一次(#3106)。hook 没有这个「每行一次」的形状,所以不能照抄。为什么现在值得单独定夺
#4775 已拍板方案 B:条件求不出值 → 抛错中断该次操作。落地之后,只要某个对象上有一条引用
previous的 hook 条件,该对象的每一次批量更新都会写入失败 —— 失败原因还指向一个跟这次写入无关的 hook。#4784 修好的是「照文档写的 hook 不再静默失效」,这一格是它剩下的、会在 #4775 之后变成事故的部分。派发 #4784 时给的成本提示是「引用
previous会让 bulk predicate update 逐行取数」。逐行取数本身不难(复用rulesNeedRows那条路),难的是取回来之后绑给谁 —— 一次 hook 调用配 N 行前态。所以 #4799 的文档写的是实际行为(bulk 上不绑定),没有写那句提示,以免又一次 declared ≠ delivered。可选方案(两条轴)
A. 批量写上按行触发带
previous条件的 hook引用
previous的条件 → 逐行取 prior → 该 hook 按行求值、按行触发,ctx.previous= 该行。ctx.result的形状也要跟着定(整批 vs 单行)。只对引用previous的 hook 这么做会更糟:行为取决于条件文本,那是隐性规则。要做就得想清楚是否对所有 after 型 hook 统一按行。B. 明确批量上
previous不可用,并给一条专门的诊断保持不绑定,但在 #4775 的 fail loud 里为这一格出一条点名的错误(「hook 条件引用了
previous,但这是一次 predicate 批量更新,没有单一前置记录;请改用单记录写入,或把过渡语义放到 record-change flow trigger」),而不是默认的No such key: previous。C. 批量写上跳过引用
previous的条件(视作不适用,不阻断写入)condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 判过死刑的「静默当 false」,只是换了个名字。建议
A,若一次做不动则先上 B(它与 #4775 同批即可完成,成本低),把 A 留给「after 型 hook 在批量写上是否统一按行触发」那次更大的定夺。理由沿两条轴:长远上 A 才是让 hook 与 validation 对同一件事给同一个答案(#3106 已经在 validation 侧付过这笔设计成本);防 AI 写错上,A 让作者只需要记一套过渡写法,而 B 要求他们记住一条例外 —— 但 B 至少让例外响亮,远好过今天的静默。
关联
condition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784 / PR fix(objectql,skills): hookcondition的 CEL 作用域补上previous—— 过渡语义可写,与 validation 谓词对齐 (#4784) #4799(previous进 hook condition 作用域)condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 落地前需要有答案,否则批量写入会因为一条无关的 hook 条件而失败previous总全 + fail closed)