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

claude added 2 commits August 3, 2026 07:06
…可写,与 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

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 7:07am

Request Review

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

github-actions Bot commented Aug 3, 2026

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.

Copy link
Copy Markdown
Contributor Author

范围外发现,已按 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants