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

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions