Skip to content

Three provenance-stamp beforeUpdate hooks still carry the pre-#6966 "no input.id ⇒ bulk write" guard and re-read a row the engine already bound #15302

Description

@claude

Found while measuring the #14744 census (PR #15301); ⛔ not fixed there — out of that card's measurement-only scope. Unassigned and untriaged.

What is stale

Three provenance-stamp beforeUpdate hooks carry the same two comments, and runtime measurement falsifies both:

  • packages/plugins/plugin-email/src/email-template-provenance.ts:25 and :65
  • packages/plugins/plugin-sharing/src/sharing-rule-provenance.ts:25 and :53
  • packages/plugins/plugin-webhooks/src/webhook-provenance.ts:24 and :56

Claim 1 — "Known boundary: multi-row updates (no single input.id) are not stamped". False since #6966. Per-row before* dispatch binds ctx.input.id on every context, so the if (!id) return guard no longer detects a bulk write — the hook runs once per matched row. #6966 is exactly this defect and was fixed in service-storage's file-reference-lifecycle.ts (which now asks perRowDispatch(ctx), the engine's marker); these three were not carried along.

Claim 2 — "previous is not resolved before beforeUpdate hooks run — read the current row ourselves". False since #5574 / #5846: the engine binds hookContext.previous before dispatching beforeUpdate on every write shape, including each per-row context of a predicate write. #6656 retired plugin-audit's captureBefore for precisely this reason. Each of these three still issues its own engine.find(...) per row — a second read of a row the engine has already read.

Measured

With the real engine (packages/objectql/src/engine.ts) and the real bind functions, two rows updated in one multi: true call:

rows observed
disagree on managed_by row a writes customized: true, row b writes nothing ⇒ the batch is refused: MULTI_UPDATE_HOOK_KEY_DIVERGENCE, 400, keys: ['customized'], rows: 2
agree on managed_by both rows write customized: true; one SET clause; the write lands

So the documented "not stamped on multi-row updates" boundary is not what happens: the hooks fire per row, and since #14099 landed they can now refuse an operator's bulk edit of package-managed rows. That refusal is correct engine behaviour — the problem is that these hooks' own contract says they take no part in multi-row updates, so neither their authors nor their callers expect it.

Reproduce with the census probe on PR #15301:

npx tsx scripts/audits/14744-before-update-per-row-value-probe.mjs --out /tmp/probe.json

Why this is worth a card

⛔ Not a per-row-value instance — the value written is the constant true, so this is not #14744's residue and the census counts it as a non-instance. It is the residue of two closed cards whose remediation did not reach these three files:

  1. a guard that no longer guards (#5574's per-row before* dispatch binds input.id on every context, silently changing the semantics of every "no id ⇒ skip, this is a bulk write" hook guard #6966), and
  2. a redundant per-row read the engine already performs (plugin-audit captureBefore still fetches its own pre-image — retire the second read once the engine binds ctx.previous before every before* dispatch #6656 / 单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846), which on a bulk edit is one extra find per matched row.

Suggested shape (not a decision)

Ask the engine's marker rather than the shape of input.idperRowDispatch(ctx), as file-reference-lifecycle.ts does — and read ctx.previous instead of re-reading the row. Whether these hooks should stamp per-row at all, or decline on a predicate write, is a product question and is left open here.


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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions