Skip to content

fix(objectql,skills): hook condition 的 CEL 作用域补上 previous —— 过渡语义可写,与 validation 谓词对齐 (#4784) - #4799

Merged
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4784-hook-condition-previous-scope
Aug 3, 2026
Merged

fix(objectql,skills): hook condition 的 CEL 作用域补上 previous —— 过渡语义可写,与 validation 谓词对齐 (#4784)#4799
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4784-hook-condition-previous-scope

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes#4784

按维护者拍板的方案 A:hook condition 的作用域扩为 record + previous,与 validation 谓词对齐。

为什么

条件求值只绑一个根(hook-wrappers.tsevaluate(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,条件求值绑定两个根:

取不到 prior 时 previous 不绑定(CEL 里就是一个未声明标识符),与 validation/rule-validator.ts 逐字一致:

事件 / 表面previous
单记录 update 的 hook 条件、update 上的 validation 规则写前的存储行
insert 事件(beforeInsert / afterInsert)不绑定 —— 没有前态
predicate(multi: true)批量更新不绑定 —— 一次匹配 N 行、hook 只触发一次,没有单一前置记录

{} / 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.mdcondition 一节同步(它此前写着「条件只能描述状态,不能描述 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 记录,见下。

验证

pnpm --filter @objectstack/objectql test # 108 files / 1700 tests passed
pnpm --filter @objectstack/objectql typecheck # clean
pnpm --filter @objectstack/spec check:generated # 8/8 up to date
pnpm --filter @objectstack/spec check:skill-examples # 202 prose examples type-check
pnpm --filter @objectstack/example-showcase verify # validate + typecheck + 60 tests passed
pnpm --filter @objectstack/runtime exec vitest run src/sandbox # 110 passed

新增 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

…可写,与 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
@vercel

vercelBot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 3, 2026 7:07am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@xuyushun441-sysClaude

Copy link
Copy Markdown
ContributorAuthor

范围外发现,已按 Prime Directive #10 单独立 issue(未指派):#4800 —— predicate 批量更新上 hook conditionprevious 语义

那一格正是 #4784 原文点名「要先定『拿不到时 previous 是什么』」的部分,而拍板意见里没有覆盖:hook 在批量写上只触发一次,N 行没有单一的 previous 可绑。本 PR 因此保持不绑定并如实写进文档,没有替它编一个答案。它需要在 #4775 之前有结论 —— fail loud 落地后,一条引用 previous 的 hook 条件会让该对象的批量更新写入失败,失败原因还指向一个与这次写入无关的 hook。

更正一处笔误:新增测试文件是 15 个用例(wrapper 级 9 + 真实引擎 4 + 取数计数钉子 2),不是正文写的 19;与既有的 hook-condition-merged-record.test.ts 一起跑是 27 个,全绿。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@xuyushun441-sys@claude