docs(skills): data-hooks 的 condition 一节按 before/after 拆开重述批量写绑定 (#5900) - #5989
Merged
Conversation
`skills/objectstack-data/references/data-hooks.md` 的 `condition` CEL 绑定一节 仍按 #5038 之前教「`multi: true` 批量写只匹配 N 行、hook 只触发一次,`previous` 无从绑定」,并在结尾明令「never reach for `previous` in a hook that can fire on insert or on a `multi: true` write」。这对 `before*` 成立,对 `after*` 不成立 —— 后者正是 #5038(ADR-0058 批量写增补)修掉的行为,平台现在按匹配行派发,并有 `packages/objectql/src/bulk-write-per-row-hooks.test.ts` 逐条钉死。危害方向是 skill **明令禁止**平台已支持且已 pin 的模式:按它写的 AI 作者会给批量写路径手工 绕开 transition condition,或干脆不给批量写挂 `after*` 审计/通知 hook —— 而 #5038 的原始动机恰恰是那类 automation「静默不发生」。 按 issue 的验收口径拆成三段,每句都对应仓内一条绿断言: - `before*`(含批量)整批触发一次,`previous` 无从绑定 —— 保留,并补上「为什么」 (`before*` 仍可改写共享 payload,一个批次只有一份 payload)与该处 `record` 是 裸 payload 这一同源事实;读 `previous` 会被具名拒收并指向 after 事件。 - 新增一条:`after*` 在批量写上**按匹配行**派发,`previous` 是该行 pre-image, `record` 是该行真实状态(非裸 payload),`input.id` 命名该行(#5038)—— transition condition 因此在 `afterUpdate` / `afterDelete` 上**可用**,写法与 单记录写完全一致。 - 插入事件(`beforeInsert` / `afterInsert`)`previous` 无从绑定 —— 保留,与批量 无关。 #4775 那段(不可求值的 condition 中止写入)语义未动;只把其中「the two bullets above」改成按名指代(unbound-`previous` 与 `has(...)` 两条),因为上面由两条 bullet 变成了三条,计数指代会失真。 不新增测试没钉住的断言:三段措辞逐句对应 `bulk-write-per-row-hooks.test.ts`(按行派发、每行绑定、`record` 为真实状态、 `input.id` 命名该行)与 `hook-condition-bulk-previous.test.ts`(`before*` 批次 派发具名拒收 `bulk_write_previous_unbound`、`record` 为裸 payload 时的 `bulk_write_stored_state_unavailable`、仍然中止写入),两文件本次实测 44 条全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 6, 2026 14:10
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 #5900
纯散文修正:
skills/objectstack-data/references/data-hooks.md的conditionCEL 绑定一节仍按 #5038 之前教「multi: true批量写只触发一次、previous无从绑定」,并结尾明令作者「never reach forpreviousin a hook that can fire on insert or on amulti: truewrite」。这对before*成立,对after*不成立。不动引擎、不动 spec、不动 releases。前提核实(对
origin/main=628b028):258-262、:279-282,issue 正文写的是:254-257/:274-278)after*不成立bulk-write-per-row-hooks.test.ts全文钉死按行派发,其常量TRANSITION = 'record.status == "done" && previous.status != "done"'正是 skill 明令禁止的写法,且按行求值通过hook.zod.ts已按 #5038 措辞packages/spec/src/data/hook.zod.ts:352-357:「theafter*events on a bulk write dispatch ONCE PER MATCHED ROW …input.idnames that row,previousis its pre-image andresultits post-state」before*那半句仍真hook-condition-bulk-previous.test.ts专门钉死这条不对称:before*批次派发仍具名拒收(limitation: 'bulk_write_previous_unbound')并中止写入:65-66(#5901 刚落地)已经写着after*按匹配行派发,而:261-262还写着「the hook fires once」无断言被证伪。 唯一偏差是行号漂移,已按内容定位。
改了什么
按 issue 的验收口径拆成三段,原来的一条 bullet 变成两条:
before*(含批量)整批触发一次,previous无从绑定 —— 保留,并补上「为什么」:before*仍可改写共享 payload,而一个批次只有一份 payload,所以没有按行的东西可交给它;同时补上同源事实 —— 那个派发上record是裸 payload,故这次写入未设置的已声明字段同样不可求值。读previous会被具名拒收,并被指向 after 事件。after*在批量写上按匹配行派发([17.x] 批量写按行语义实现:hook 按行触发 + record-change trigger 按行绑定 previous/record(#4800/#4862 拍板 A) #5038) ——previous是该行 pre-image,record是该行真实状态(不是裸 payload),input.id命名该行;因此 transition condition 在afterUpdate/afterDelete上是可用的,写一次即可,不需要任何 bulk 分支。beforeInsert/afterInsert)previous无从绑定 —— 保留,与批量无关。结尾那段「Practical consequence」从「凡是可能批量触发就别用
previous」改为「把读previous的 condition 放在 after 类事件上 —— 别放在插入事件,也别放在可能被multi: true命中的before*hook 上」。一处被迫的连带修改
#4775那段(不可求值 condition 中止写入)语义未动,只把其中「the two bullets above are load-bearing」改成按名指代(unbound-previous与has(...)两条)。上面由两条 bullet 变成三条,计数式指代会失真;按名指代同时对将来的增删免疫。措辞不自行发明:与
skills/objectstack-formula/SKILL.md:311-313已落地的那张绑定表、hook.zod.ts:352-357、hook-wrappers.ts:625-640/:930-937的注释同源。验证 —— 反向验证方向说明(重要)
本单没有可 revert 的代码,所以模板里「恢复删掉的分支 → 新测试转红」那条对它不适用;照那个形状伪造一段证据比留空更糟。这里的真值验证是另一条:我写进文档的每一句,都对应仓内一条已绿的 pin 断言。两份 pin 测试全文实测:
句子 → 断言的映射(全部来自这 44 条):
after*按匹配行派发a bulk write fires after-hooks once per matched row > N matched rows ⇒ N dispatches;… > is UNIFORM — a condition that never mentions previous fires per row tooprevious是该行 pre-imageeach dispatch carries THAT row's previous / record > previous is the row's own pre-write state, not a shared onerecord是该行真实状态、非裸 payload… > record is the row's REAL state, not the bare payload (#4862 fact 4)input.id命名该行… > input.id names the row, giving the single-record context shape (#2922)after*上可用… > the condition discriminates per row — only real transitions fire;the diagnostic is RETIRED for after-type hooks > the bulk write that #5037 rejected now succeeds, firing the hook per rowbefore*整批触发一次、previous无从绑定the batch dispatch of a bulk write, whose condition reads previous > rejects with a machine-readable limitation, not just prose(bulk_write_previous_unbound)… > names the batch, the missing binding, and the PHASE as the reason… > leads with the after-type event — the route the contract just made real;… > names beforeDelete for a delete-shaped batch dispatchrecord是裸 payload,已声明未设置字段同样不可求值a batch dispatch with no previous in the condition is unaffected > keeps the DECLARED-but-unset field diagnosis on its own limitation name(bulk_write_stored_state_unavailable)… > still ABORTS the write — the rescoping is not an exemption⛔ 没有引入测试没钉住的新断言:上表左列每一句都能落到右列;反过来,凡钉子没覆盖的(例如批量写的性能护栏、行数上限)都没写进 skill。
门禁与测试
pnpm check:nul-bytes[\x00-\x08\x0b\x0c\x0e-\x1f\x7f],零命中)pnpm check:doc-authoringpnpm check:docs-audit-scopepnpm check:skill-frame-syncpnpm check:role-wordpnpm check:adr-anchorspnpm check:org-identifier/check:release-notes/check:published-files--filter @objectstack/spec check:skill-docs--filter @objectstack/spec check:skill-refs--filter @objectstack/spec check:skill-examplespnpm lint(eslint,--no-inline-config)--filter @objectstack/objectql test--filter @objectstack/objectql typecheckchangeset:无 → 已自行加
skip-changeset标签照抄同文件同性质的前单 PR #5901(#5670,
c15fcee,今日已合入):无 changeset,labels 为documentation+skip-changeset。理由同源 —— 本 PR 零包源码改动,不发布任何 npm 包内容,按.github/workflows/pr-automation.yml(#5292 / PR #5467)的处方取路线 2(skip-changeset标签,标注PREFERRED);路线 3(空 frontmatter changeset)在同一处方里是LAST RESORT,因为它是 changesets/action 的真实输入(#4898)。metadata.version未 bump:本单未触碰任何SKILL.md,且skills/objectstack-data/内容修改的历史先例(72c3c8613、cbb6a5ca2、28ad90e9a,其中cbb6a5ca2改的正是本文件)均未 bump。顺带发现
无。 全仓复查同句(
grep -rniE "never reach for .previous|previous. is unbound|no single prior record|hook fires once",排除node_modules与测试):skills/objectstack-formula/SKILL.md:313、content/docs/data-modeling/formulas.mdx:381、packages/objectql/src/hook-wrappers.ts三处均已按before*限定,措辞正确;packages/objectql/CHANGELOG.md是已发布的历史记录,按 CLAUDE.md 不在代码 PR 里改。派发面点名排除的skills/objectstack-data/rules/hooks.md经核实不含本单点名的措辞(它那句previous(undefinedon insert)只讲插入,仍真;:115-122的批量段落已由 #5901 更新到 #5038 口径),故无需搭车、也无可立项。Generated by Claude Code