fix(objectql): hook 条件对完整 record 求值 —— stored ⊕ payload,与 #4649 同源 (#4770) - #4786
Merged
xuyushun441-sys merged 1 commit intoAug 3, 2026
Merged
Conversation
…4770) A declarative hook `condition` was evaluated against `ctx.input.data` — the fields the current write happens to carry. `ctx.previous` sat behind it, unreachable, and the two were never merged, so a condition could only reference a field the update touched. Anything else aborted the CEL expression with `No such key`, which the gate swallowed into `false` plus one WARN line. For a guard-style hook that reads as "let it through"; for an audit-style hook it reads as "do not record it". `showcase_audit_task_completion` (`record.done == true`) therefore did NOT run on the most ordinary updates there are — change the status, change the assignee — and the only trace was ten warn lines per showcase boot. The record a condition reads is now built exactly the way a validation predicate's is (#1871 / #4649): stored ⊕ payload, made total over the object's DECLARED fields, `null` when a declared key is in neither. `materializeDeclaredFields` moves out of `validation/rule-validator.ts` into `declared-fields.ts` and is shared by both paths, because `record.done == true` must not mean two different things depending on which surface evaluates it. Declared-only is the load-bearing half: a typo'd or undeclared key stays unevaluable and is still reported. Materialisation runs only when the persisted state is in hand — an insert, or an update whose prior row was fetched — so a predicate bulk update keeps its payload rather than gaining nulls that contradict N stored rows. `ctx.ql.getObject()` is an in-memory registry read; no code path fetches a record it did not already load. Out of scope, deliberately unchanged: what happens when a condition is STILL unevaluable after merging (warn + treat as false). Its failure direction is opposite for guard and audit hooks and is tracked separately. `has(...)` is not a null guard once the record is total — the showcase's over-budget condition is rewritten with `!= null`, and the hook docs that prescribed `has(record.x)` are corrected with it. Evidence: `pnpm dev -- --fresh` on this branch logs 0 `condition evaluation failed` lines; the same boot with the fix reverted logs exactly the 10 the issue reported. 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
|
Contributor
📓 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:
|
This was referenced Aug 3, 2026
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 06:26
xuyushun441-sys
deleted the
claude/issue-4770-hook-condition-merged-record
branch
August 3, 2026 06:38
os-zhuang
pushed a commit
that referenced
this pull request
Aug 3, 2026
合并 #4768(C17)/ #4786 / #4780 / #4783(#4634,31 位能力位退役)后重生成。 按 #4535 §7 与「rebase/merge 静默回退」纪律处理三张 ratchet: `dual-source-exports.baseline.json` / `authorable-surface.json` / `json-schema.manifest.json`(+ `api-surface.json`)一律 `git checkout origin/main --` 取 main 版本后全量重跑生成器,只重施本 PR 的一处改动。 其中 json-schema.manifest.json 与 authorable-surface.json 归 os-regen merge driver 管、合并不产生冲突标记,最易静默回退,故以 gen:schema 实跑 输出为准。 逐项确认他人蓄意变更未被回滚(regen 后实测): - #4783 `data/DriverCapabilities:*` 31 行 [RETIRED] + 3 行存活 —— 均在 - C10 `system/EnvironmentArtifact*` 删除 —— 仍为 0 - C17 `studio/ActionLocation` → `studio/ActionContributionLocation` —— 旧 0 新 1 - 本 PR `kernel/PackageDependency` → `kernel/ResolvedPackageDependency` —— 旧 0 新 1 `renamed-defs.ts` 冲突为两条独立改名条目并存(C17 与本簇),按时序保留两条。 dual-source 基线 2 → **0**(entries: []),#4535 第二批收官、双源账目归零。 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4770
问题
声明式 hook 的
condition一直是拿ctx.input.data(只有本次改动的字段)求值的。ctx.previous排在它后面,永远够不到,两者从不合并 —— 所以一个条件只能引用「本次恰好被改到」的字段;引用别的字段就No such key,被hook-wrappers.ts的 catch 吞成false,只留一条 warn。对 guard 型 hook 这读作「放行」,对审计型 hook 读作「不记录」。
showcase_audit_task_completion(record.done == true)因此在最常见的更新上根本不跑 —— 改个 status、改个 assignee,payload 里没有done—— 而 showcase 每次启动刷 10 条 warn 就是它唯一的痕迹。修法(本 PR 只做 issue 里的「修法 1」)
条件读到的 record 现在与 validation 谓词逐字同源(#1871 / #4649):
null,于是「表达式可不可求值」不再取决于 driver 存了哪些列;materializeDeclaredFields从validation/rule-validator.ts提到declared-fields.ts,两条路径共用同一个 helper ——record.done == true不能因为「谁在求值」而有两种含义,这正是两边最容易漂移的地方。物化只在持久状态确实在手时发生:insert(缺失就是真的没有值),或取到了 prior row 的 update。predicate(
multi: true)批量更新拿不到 prior row,于是原样保留 payload,而不是补出一堆与 N 行存储状态相矛盾的null。ctx.ql.getObject()是内存里的 registry 读取 —— 没有任何代码路径为此多读一行数据。明确不在本 PR 内(按 PM 分派范围)
「合并之后仍然求不出值」时的兜底(warn + 当作 false)一字未动。它对 guard 型和审计型 hook 的失败方向相反,属于公开契约层面的决定,由维护者单独定夺。
顺带修正的两处「declared ≠ delivered」
record 变总全之后,
has(x)不再是 null guard(声明字段哪怕值为 null 也是 present,null > null照样炸)—— 与 #4649 学到的是同一课:examples/app-showcase的 over-budget 条件改用!= null;skills/objectstack-data/references/data-hooks.md里「用has(record.x)兜住未写入字段」的处方随之更正,并写清「条件描述的是记录的状态,不是本次 diff」;skills/objectstack-automation/SKILL.md那句「hooks: the write payload」同步更新。验证
pnpm dev -- --fresh(showcase,fresh 目录 = issue 里rm -rf .objectstack的等价物):condition evaluation failed条数{"hook":"showcase_audit_task_completion","condition":"record.done == true","error":"No such key: done"})回归测试:
packages/objectql/src/hook-condition-merged-record.test.ts(12 例,含真引擎 + 只存已写列的 in-memory driver 的集成级 showcase 复现)。先验:把hook-wrappers.ts的改动 stash 掉后 12 例中 8 例失败。pnpm --filter @objectstack/objectql typecheck干净;@objectstack/spec check:generated8/8 up to date(改了 SKILL.md);check:skill-examples202 例通过。相关发现(未在本 PR 内修复)
condition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784 —— hookcondition的 CEL 作用域只绑定record,而两处已发布 skill 文档教作者写previous.*/ctx.record。本 PR 之后这条更显眼:条件表达的是状态,「刚刚变成 done」这种过渡语义在 hook 条件里仍然写不出来。🤖 Generated with Claude Code
https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
Generated by Claude Code