TL;DR
声明式 hook 的 condition 是拿 update payload (只有本次改动的字段)求值的,不是完整记录。条件里引用了一个本次没改的字段 → No such key → 被 catch 成 false → hook 该触发时不触发,只留一条默认日志级别下会被当噪音划过去的 warn 。
50185a8("fail closed on unevaluable validation predicates, and make the merged record total " , #4649 )刚给 validation 谓词修了同一类问题。hook 条件这条路径没跟上,而且它是静默失败。
现场
pnpm dev(showcase)每次启动刷 10 条:
WARN [hook] condition evaluation failed; treating as false
{"hook":"showcase_audit_task_completion","condition":"record.done == true",
"error":"No such key: done\n\n> 1 | record.done == true\n ^"}
showcase_task 确实有 done 字段 —— 不是数据模型问题:
// examples/app-showcase/src/data/objects/task.object.ts
done → boolean
根因
packages/objectql/src/hook-wrappers.ts:327
function pickRecordPayload ( ctx : HookContext ) : any {
const input : any = ctx . input ?? { } ;
if ( input && typeof input === 'object' && input . data && typeof input . data === 'object' ) {
return input . data ; // ← 只有本次 update 的字段
}
if ( ctx . previous && typeof ctx . previous === 'object' ) {
return ctx . previous ; // ← 够不到:上面已经 return 了
}
return input ;
}
input.data 的优先级高于 ctx.previous,而且两者从不合并 。于是一个 afterUpdate hook 的条件只能引用"本次恰好被改到"的字段;引用任何其它字段都会炸。
炸了之后(hook-wrappers.ts:86):
const r = ExpressionEngine . evaluate < boolean > ( expr , { record : record ?? { } } ) ;
if ( ! r . ok ) {
logger . warn ( '[hook] condition evaluation failed; treating as false' , { ...} ) ;
return false ; // ← 吞掉
}
为什么这比噪音危险
showcase_audit_task_completion 是个审计 hook。它的语义是"任务被标记完成时留痕"。现在的行为是:只要那次 update 的 payload 里没带 done,审计就静默不发生 —— 而这恰恰是最常见的情况(改个 status、改个 assignee)。审计记录缺失,且没有任何东西会告诉你缺了。
"treating as false" 对不同 hook 意味着相反的风险方向:对一个 guard 型 hook 是放行,对一个审计型 hook 是漏记。无论哪种,"表达式求不出值"和"表达式求出 false"被压成了同一个结果 。
修法
让 record 完整 :与 Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 对齐 —— 把 ctx.previous 和 input.data 合并后再求值(stored ⊕ payload),这样条件引用任何已存在字段都成立。
区分"求不出值"和"值为 false" :至少不要静默。Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 对 validation 选的是 fail closed;hook 的正确方向取决于 hook 类别,需要定夺 —— 但当前"一律吞成 false + warn"肯定不是答案。
建议 1 先做,它把绝大多数 case 直接消灭掉;2 作为兜底语义单独讨论。
复现
rm -rf examples/app-showcase/.objectstack && pnpm dev
启动日志里搜 condition evaluation failed。
TL;DR
声明式 hook 的
condition是拿 update payload(只有本次改动的字段)求值的,不是完整记录。条件里引用了一个本次没改的字段 →No such key→ 被 catch 成false→ hook 该触发时不触发,只留一条默认日志级别下会被当噪音划过去的 warn。50185a8("fail closed on unevaluable validation predicates, and make the merged record total", #4649)刚给 validation 谓词修了同一类问题。hook 条件这条路径没跟上,而且它是静默失败。现场
pnpm dev(showcase)每次启动刷 10 条:showcase_task确实有done字段 —— 不是数据模型问题:根因
packages/objectql/src/hook-wrappers.ts:327input.data的优先级高于ctx.previous,而且两者从不合并。于是一个afterUpdatehook 的条件只能引用"本次恰好被改到"的字段;引用任何其它字段都会炸。炸了之后(
hook-wrappers.ts:86):为什么这比噪音危险
showcase_audit_task_completion是个审计 hook。它的语义是"任务被标记完成时留痕"。现在的行为是:只要那次 update 的 payload 里没带done,审计就静默不发生 —— 而这恰恰是最常见的情况(改个 status、改个 assignee)。审计记录缺失,且没有任何东西会告诉你缺了。"treating as false" 对不同 hook 意味着相反的风险方向:对一个 guard 型 hook 是放行,对一个审计型 hook 是漏记。无论哪种,"表达式求不出值"和"表达式求出 false"被压成了同一个结果。
修法
ctx.previous和input.data合并后再求值(stored ⊕ payload),这样条件引用任何已存在字段都成立。建议 1 先做,它把绝大多数 case 直接消灭掉;2 作为兜底语义单独讨论。
复现
rm -rf examples/app-showcase/.objectstack && pnpm dev启动日志里搜
condition evaluation failed。