fix(objectql,skills): hook condition 的 CEL 作用域补上 previous —— 过渡语义可写,与 validation 谓词对齐 (#4784) - #4799
Conversation
…可写,与 validation 谓词对齐 (#4784) 声明式 hook 的 `condition` 只绑定一个根 `record`,但两处已发布的 skill 文档 (`objectstack-formula` §5 与它的 `ISCHANGED(x)` → `previous.x != record.x` 迁移条目)一直教作者写 `previous.*`。照着写下去只会静默失效: `No such key: previous` → 被 catch 成 `false` → hook 不触发,只留一条 warn。 declared ≠ delivered。 #4770 之后这条缺口变成了能力缺口:`record` 现在表示记录的**状态**, `record.done == true` 对每一次已完成任务的 update 都为真。「刚刚变成 done」 只能靠比较 `previous` 表达,而 `showcase_audit_task_completion` 的 description 写的正是 "after a task transitions to done"。 - `hook-wrappers.ts`:条件求值绑定 `record` + `previous` 两个根。`previous` 复用 `materializeDeclaredFields`(#4649/#4770 的同一个 helper)对**已声明字段** 做成总全 —— driver 没返回的列读作 `null` 而不是让整条表达式 fault;未声明的 key 仍然不可求值,拼写错误照旧报出来。**拷贝而非原地修改**:`ctx.previous` 是引擎自己的 pre-image,after hook 观察的就是它,物化出的 null 不回灌。 - 取不到 prior 时 `previous` **不绑定**(CEL 里就是一个未声明标识符),与 `validation/rule-validator.ts` 逐字一致:insert 事件没有前态;predicate (`multi: true`) 批量更新一次匹配 N 行、hook 只触发一次,没有单一前置记录可绑。 绑 `{}`/`null` 等于替没人读过的行编造事实。 - **不新增按需取数机制**:`previous` 搭的是 `engine.update` 既有的那一次 prior 取数(注册了 afterUpdate hook 就会取),即喂 `ctx.previous` 和 record-change flow trigger 的同一行。不引用 `previous` 的条件零额外取数, 已用测试钉死。engine.ts 那处 gate 留了注释:今后若收窄它,必须把 hook 条件的 `previous` 需求算进新的判定。 - 文档:`objectstack-automation/SKILL.md` 速查表里的 `ctx.record` 是纯错 (`HookContext` 声明的是 `input` / `result` / `previous` / `session` / `ql`), 改为区分 handler 的 `ctx.*` 与 condition 的 CEL 根;`objectstack-formula` §5 保留 `previous` 示例并补上绑定范围/总全/`!= null` 而非 `has()`/成本说明; `objectstack-data/references/data-hooks.md` 的 condition 一节同步。 - showcase 的 `showcase_audit_task_completion` 改用过渡条件,让它的 description 与实际行为一致。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现,已按 Prime Directive #10 单独立 issue(未指派):#4800 —— predicate 批量更新上 hook 那一格正是 #4784 原文点名「要先定『拿不到时 更正一处笔误:新增测试文件是 15 个用例(wrapper 级 9 + 真实引擎 4 + 取数计数钉子 2),不是正文写的 19;与既有的 Generated by Claude Code |
Fixes #4784
按维护者拍板的方案 A:hook
condition的作用域扩为record+previous,与 validation 谓词对齐。为什么
条件求值只绑一个根(
hook-wrappers.ts的evaluate(expr, { record })),而两处已发布的 skill 文档一直教作者写previous.*。写下去只会静默失效:No such key: previous→ 被 catch 成false→ hook 不触发,只留一条 warn。declared ≠ delivered。#4770 之后这条缺口从「文档与运行时对不上」升级成能力缺口:
record现在表示记录的状态,record.done == true对每一次已完成任务的 update 都为真。「刚刚变成 done」只能靠比较previous表达,而showcase_audit_task_completion自己的 description 写的正是 "after a task transitions to done"。改了什么
packages/objectql/src/hook-wrappers.ts—— 新增pickPreviousPayload,条件求值绑定两个根:previous复用materializeDeclaredFields(Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 / bug(objectql): hook 条件对着「只含本次更新字段」的残缺 record 求值,取不到的字段被吞成 false —— 审计 hook 静默不触发 #4770 抽出的同一个 helper)对已声明字段做成总全 —— driver 没返回的列读作null,而不是让整条表达式 fault(在 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 的 fail loud 之下这是必需,不是优化);previous.dnoe仍然不可求值,拼写错误照旧报出来;ctx.previous是引擎自己的 pre-image,after hook 观察的就是它 —— 物化出的 null 不回灌(Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649 就此留过注释,这里用测试钉住)。取不到 prior 时
previous不绑定(CEL 里就是一个未声明标识符),与validation/rule-validator.ts逐字一致:previousbeforeInsert/afterInsert)multi: true)批量更新绑
{}/null等于替没人读过的行编造事实 —— 物化逻辑最小心避免的就是这件事。没有新增按需取数机制。
previous搭的是engine.update既有的那一次 prior 取数(needsPriorRecord(schema) || 存在 afterUpdate hook),也就是喂ctx.previous和 record-change flow trigger 的同一行。因为 afterUpdate 恰恰是 context 携带previous的那个事件,再叠一层「条件有没有引用previous」的编译期判定今天就是死代码,所以没有写。engine.ts那处 gate 留了注释:今后若收窄它(例如按 object 过滤),必须把 hook 条件的previous需求算进新的判定 —— 由测试钉住。文档
skills/objectstack-automation/SKILL.md速查表里的ctx.record是纯错(HookContext声明的是input/result/previous/session/ql),拆成 handler 的ctx.*与 condition 的 CEL 根两行,并写清「没有ctx.record这个东西」;skills/objectstack-formula/SKILL.md§5 保留previous示例与OLD.x/ISCHANGED(x)迁移条目(方案 A 之下它们变成正确的),补上绑定范围表、总全语义、「用!= null而不是has()」以及成本说明;skills/objectstack-data/references/data-hooks.md的condition一节同步(它此前写着「条件只能描述状态,不能描述 diff」)。showcase ——
showcase_audit_task_completion改用过渡条件,让它的 description 与实际行为一致。成本说明与维护者约束 5 的一处出入(请过目)
派发时给的措辞是「引用
previous会让 bulk predicate update 逐行取数」。实现下来 bulk 路径并没有这么做,原因是它需要先回答一个尚未定夺的契约问题:hook 在批量写上只触发一次,N 行没有单一的previous可绑;要让它成立,得让 hook 按行触发 —— 那是对 hook 契约的实质改动,不在本次拍板范围内。所以文档写的是实际行为(bulk 上previous不绑定、条件不可求值),而不是那句成本提示 —— 否则就是又一次 declared ≠ delivered。这条与 #4775 有交互:fail loud 落地后,一条引用
previous的 hook 条件会让该对象的批量更新写入失败。已单独立 issue 记录,见下。验证
新增
packages/objectql/src/hook-condition-previous-scope.test.ts(19 个用例,全部标注 #4784):过渡条件只在翻转那一次触发(wrapper 级 + 真实引擎 级各一组)、总全性、未声明 key 仍不可求值、不回灌ctx.previous、insert / bulk 不绑定,以及两条取数计数钉子(record-only 的 before 条件零取数;引用与不引用previous的取数次数相同)。🤖 Generated with Claude Code
https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
Generated by Claude Code