Skip to content

data-hooks skill 仍按 #5038 之前教「multi: true 批量写只触发一次、previous 无从绑定」,并明令作者不要在可能批量触发的 hook 里用 previous #5900

Description

@os-zhuang

实现 #5670 时在同一文件(skills/objectstack-data/references/data-hooks.md)另一节发现的另一条已过期陈述。PD #10 单独记录:#5670 的派发面是 ctx.input.ast 那半句,本条是不同的断言(讲 hook condition 的 CEL 绑定),不搭车。

事实(对 origin/main = 5c94f833c 核实)

skills/objectstack-data/references/data-hooks.md,condition 的 CEL 绑定一节:

:254-257

- **`previous` is UNBOUND where there is no prior state**, and a reference to an
  unbound root makes the whole condition unevaluable. That means: insert events
  (`beforeInsert` / `afterInsert`) — write those over `record` alone — and
  predicate (`multi: true`) bulk updates, where one write matches N rows and the
  hook fires once, so there is no single prior record to bind.

:274-278

Practical consequence when authoring: spell keys against the object's **declared**
fields, and never reach for `previous` in a hook that can fire on insert or on a
`multi: true` write — that mistake used to cost you a hook that quietly never
ran, and now costs you every write the hook is attached to.

这两处对 before* 成立,对 after* 不成立 —— 它们正是 #5038 修掉的那个行为。

真值来源(已在 main 上)

packages/objectql/src/bulk-write-per-row-hooks.test.ts 的文件头把旧行为写成了历史:

 * What used to happen instead, measured on #4862: `driver.updateMany` resolves
 * an affected COUNT, the lifecycle hook fired ONCE, `hookContext.previous` was
 * never assigned, and `record` degraded to the write's bare payload. So the
 * transition condition the docs and ten showcase flows teach
 * (`status == "done" && previous.status != "done"`) could not be evaluated on a
 * bulk write …

以及它钉死的现行契约:

 *   1. firing GRANULARITY — N matched rows ⇒ N dispatches, uniformly for every
 *      after-hook, never keyed on whether the condition text says `previous`;
 *   2. the per-row BINDINGS — `previous` is that row's pre-image, `record` is
 *      that row's real state (not the bare payload), `input.id` names the row;

测试里的用例常量就是 skill 明令禁止的那种写法,并且是按行求值通过的:

const TRANSITION = 'record.status == "done" && previous.status != "done"';
describe('[#5038] each dispatch carries THAT row's previous / record')
  it('`previous` is the row's own pre-write state, not a shared one')
  it('is UNIFORM — a condition that never mentions `previous` fires per row too')

packages/spec/src/data/hook.zod.ts:352-357(PR #5668 落地文本)同样说明:

   * Since #5038 (ADR-0058's bulk-write addendum) the `after*` events on a bulk
   * write dispatch ONCE PER MATCHED ROW, each on a single-record-shaped
   * context — `input.id` names that row, `previous` is its pre-image and
   * `result` its post-state …

危害方向与 #5670 相反

#5670 那句是让作者去读一个恒为 undefined 的字段;本条是反过来——skill 明令禁止平台现在已经支持、并且专门写了 pin 测试的那个模式(previous 的 transition condition)。按 skill 写的 AI 作者会为批量写路径手工绕开 transition 条件,或者干脆不给批量写挂 after* 审计/通知 hook —— 而 #5038 的原始动机恰恰是那类 automation「静默不发生」。

建议

把两处按 before / after 拆开重述:

严重程度请分诊轮判,我按发现原样提交。

关联


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions