TL;DR
声明式 hook 的 condition 求值时只绑定一个根 record:
// packages/objectql/src/hook-wrappers.ts
const r = ExpressionEngine.evaluate< boolean >(expr, { record: record ?? {} });
但两处已发布的 skill 文档教作者写 previous.*(以及一个不存在的 ctx.record)。写了就静默失效:previous.status → No such key: previous → 被 catch 成 false → hook 不触发,只留一条 warn。
现场
-
skills/objectstack-formula/SKILL.md §5「Update hook condition — previous vs record」直接给出:
P`previous.status != 'escalated' && record.status == 'escalated'`
这个例子对 validation rule 的 condition 是对的(rule-validator.ts 确实传 previous binding),对 hook 的 condition 是错的 —— 而小标题写的是 hook。同一页的转换表还把 ISCHANGED(x) 映射成 previous.x != record.x,作者会照着往 hook 里抄。
-
skills/objectstack-automation/SKILL.md 的 time-word 速查表:
| Surface |
Event-time record |
Pre-event record |
Live record |
Object hook (ctx) |
ctx.record (write payload) |
ctx.previous |
— |
HookContext 上没有 record 这个键(packages/spec/src/data/hook.zod.ts:input / result / previous / session / …)。ctx.previous 是真的,ctx.record 不是。
为什么现在值得单独立一条
#4770(hook 条件改为对 stored ⊕ payload 求值)刚把条件的语义从「本次 diff」变成「记录状态」:record.done == true 现在对每一次已完成任务的 update 都为真,而不只是那次把它置为 done 的 update。想表达「刚刚变成 done」这件事,唯一的写法就是比较 previous —— 而 hook 条件恰恰拿不到它。审计类 hook 的原意(showcase_audit_task_completion 的 description 写的是 "after a task transitions to done")因此仍然表达不出来。
两条路,需要定夺(所以本条不带实现)
- A. 给 hook condition 的作用域补上
previous(以及可能的 user),与 validation 谓词对齐。 好处:文档里已经这么教了,作者心智一致,过渡语义可写。代价:condition 的可写表达面变大,属于公开契约扩展 —— 而且 previous 只在引擎取到 prior row 时才有(bulk predicate update 没有),要先定「拿不到时 previous 是什么」。
- B. 不扩作用域,把两处文档改成只讲
record,并明确「hook 条件无法表达过渡,需要过渡语义请用 record-change flow trigger 的 start condition」。 好处:不动运行时。代价:skills/objectstack-formula 的 ISCHANGED 迁移路径在 hook 上就是断的。
无论选哪条,文档与运行时现在是对不上的,属于 declared ≠ delivered。
复现
一个 afterUpdate hook,condition: "previous.done != true && record.done == true",更新任一字段:
WARN [hook] condition evaluation failed; treating as false
{"hook":"...","condition":"previous.done != true && record.done == true",
"error":"No such key: previous..."}
发现于 #4770 的实现过程中(PR: hook 条件合并 record),按 AGENTS.md Prime Directive #10 记录,未在该 PR 内修复。
TL;DR
声明式 hook 的
condition求值时只绑定一个根record:但两处已发布的 skill 文档教作者写
previous.*(以及一个不存在的ctx.record)。写了就静默失效:previous.status→No such key: previous→ 被 catch 成false→ hook 不触发,只留一条 warn。现场
skills/objectstack-formula/SKILL.md§5「Update hook condition —previousvsrecord」直接给出:这个例子对 validation rule 的 condition 是对的(
rule-validator.ts确实传previousbinding),对 hook 的 condition 是错的 —— 而小标题写的是 hook。同一页的转换表还把ISCHANGED(x)映射成previous.x != record.x,作者会照着往 hook 里抄。skills/objectstack-automation/SKILL.md的 time-word 速查表:ctx)ctx.record(write payload)ctx.previousHookContext上没有record这个键(packages/spec/src/data/hook.zod.ts:input/result/previous/session/ …)。ctx.previous是真的,ctx.record不是。为什么现在值得单独立一条
#4770(hook 条件改为对 stored ⊕ payload 求值)刚把条件的语义从「本次 diff」变成「记录状态」:
record.done == true现在对每一次已完成任务的 update 都为真,而不只是那次把它置为 done 的 update。想表达「刚刚变成 done」这件事,唯一的写法就是比较previous—— 而 hook 条件恰恰拿不到它。审计类 hook 的原意(showcase_audit_task_completion的 description 写的是 "after a task transitions to done")因此仍然表达不出来。两条路,需要定夺(所以本条不带实现)
previous(以及可能的user),与 validation 谓词对齐。 好处:文档里已经这么教了,作者心智一致,过渡语义可写。代价:condition 的可写表达面变大,属于公开契约扩展 —— 而且previous只在引擎取到 prior row 时才有(bulk predicate update 没有),要先定「拿不到时previous是什么」。record,并明确「hook 条件无法表达过渡,需要过渡语义请用 record-change flow trigger 的 start condition」。 好处:不动运行时。代价:skills/objectstack-formula的ISCHANGED迁移路径在 hook 上就是断的。无论选哪条,文档与运行时现在是对不上的,属于 declared ≠ delivered。
复现
一个
afterUpdatehook,condition: "previous.done != true && record.done == true",更新任一字段:发现于 #4770 的实现过程中(PR: hook 条件合并 record),按 AGENTS.md Prime Directive #10 记录,未在该 PR 内修复。